目录
hashmap底层原理
B+树底层原理
两种查询方式
ConcurrentHashMap
hashtable
hashtable插入数据
ConcurrentHashMap
多线程
说一下你对线程池的理解
常用的线程池有哪些?
线程池原理知道吗?以及核心参数
sleep()和wait() 有什么区别?
notify()和notifyAll()有什么区别?
Java中synchronized 和 ReentrantLock 有什么不同?
锁优化机制
hashmap底层原理
1、map.put(k,v)实现原理
(1)首先将k,v封装到Node对象当中(节点)。
(2)然后它的底层会调用K的hashCode()方法得出hash值。
(3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
2、map.get(k)实现原理
(1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
- 红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
- 链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);
B+树底层原理
B+树是B树的一种变体,有着比B树更高的查询性能,都是为磁盘等外存储设备设计的一种平衡查找树.
系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,page(页)是其磁盘管理的最小单位(InnoDB存储引擎中默认每个页的大小为16KB).
定义一条记录为一个二元组[key, data]
B树每个节点中不仅包含数据的key值,还有data值。
而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。B+Tree的高度一般都在2~4层
两种查询方式
根节点指针指向叶子节点,叶子节点(即数据节点)之间是一种链式环结构,因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
聚簇索引和非聚簇索引
聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个
聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续
聚集索引:物理存储按照索引排序;聚集索引是一种索引组织形式,索引的键值逻辑顺序决定了表数据行的物理存储顺序。
非聚集索引:物理存储不按照索引排序;非聚集索引则就是普通索引了,仅仅只是对数据列创建相应的索引,不影响整个表的物理存储顺序。
索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
ConcurrentHashMap
hashtable
Hashtable所有的方法都是同步的,因此,它是线程安全的。
Hashtable是通过“拉链法”实现的散列表,因此,它使用数组+链表的方式来存储实际的元素
hashtable插入数据
当向Hashtable中插入数据的时候,首先通过键的hashCode和Entry数组的长度来计算这个值应该存放在数组中的位置index。如果index对应的位置没有存放值,则直接存放到数组的index位置即可,当index有冲突的时候,则采用“拉链法”来解决冲突。
Hashtable通过使用synchronized修饰方法的方式来实现多线程同步,因此,Hashtable的同步会锁住整个数组。在高并发的情况下,性能会非常差,Java5中引入了ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap采用了更细粒度的锁来提高在并发情况下的效率。
它由Segment数组结构和HashEntry数组结构组成,Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
ConcurrentHashMap将Hash表默认分为16个桶,大部分操作都没有用到锁,而对应的put、remove等操作也只需要锁住当前线程需要用到的桶,而不需要锁住整个数据
在大并发的情况下,同时可以有16个线程来访问数据,大大提高了并发性.
多线程
说一下你对线程池的理解
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降
低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
常用的线程池有哪些?
1 newSingleThreadExecutor :创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线 程达到线程池的最大大小。
2 newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM )能够创建的最大线程大小。
3 newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
4 newSingleThreadExecutor :创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池原理知道吗?以及核心参数
首先线程池有几个核心的参数概念:
1. 最大线程数 maximumPoolSize
2. 核心线程数 corePoolSize
3. 活跃时间 keepAliveTime
4. 阻塞队列 workQueue
5. 拒绝策略 RejectedExecutionHandler
当提交一个新任务到线程池时,具体的执行流程如下:
1. 当我们提交任务,线程池会根据 corePoolSize 大小创建若干任务数量线程执行任务
2. 当任务的数量超过 corePoolSize 数量,后续的任务将会进入阻塞队列阻塞排队
3. 当阻塞队列也满了之后,那么将会继续创建 (maximumPoolSize-corePoolSize)个数量的线程来
执行任务,如果任务处理完成, maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
4. 如果达到 maximumPoolSize ,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
sleep()和wait() 有什么区别?
对于 sleep() 方法,我们首先要知道该方法是属于 Thread 类中的。而 wait() 方法,则是属于 Object 类
中的。
sleep() 方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持
者,当指定的时间到了又会自动恢复运行状态。在调用 sleep() 方法的过程中,线程不会释放对象
锁。
当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用
notify() 方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
notify()和notifyAll()有什么区别?
notify 可能会导致死锁,而 notifyAll 则不会 任何时候只有一个线程可以获得锁,也就是说只有一
个线程可以运行 synchronized 中的代码使用notifyall, 可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
wait() 应配合 while 循环使用,不应使用 if ,务必在 wait() 调用前后都检查条件,如果不满足,必须调
用 notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。 notify()是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify() 下一个线程,并且自身需要重新回到 WaitSet中
.
Java中synchronized 和 ReentrantLock 有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如
果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的
区别:
这两种方式最大区别就是对于 Synchronized 来说,它是 java 语言的关键字,是原生语法层面的互
斥,需要 jvm 实现。而 ReentrantLock 它是 JDK 1.5 之后提供的 API 层面的互斥锁,需要 lock() 和 unlock()方法配合 try/fifinally 语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成 monitorenter 和 monitorexit 这个两个字节码指令。在执行monitorenter 指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1 ,相应的,在执行 monitorexit 指令时会将锁计算器就减1 ,当计算器为 0 时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另
一个线程释放为止。 由于ReentrantLock 是 java.util.concurrent 包下提供的一套互斥锁,相比Synchronized , ReentrantLock类提供了一些高级功能,主要有以下3 项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized 来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平
锁, ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁
表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock 对象可以同时绑定多个对象
锁优化机制
从 JDK1.6 版本之后, synchronized 本身也在不断优化锁的机制,有些情况下他并不会是一个很重量 级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。 锁的状态从低到高依次为无锁 - > 偏向锁 - > 轻量级锁 - > 重量级锁 ,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
自旋锁 :由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起
线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,
可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开
启,自旋的默认次数是 10 次,可以使用-XX:PreBlockSpin设置。
自适应锁 :自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上
的自旋时间和锁的持有者状态来决定。
锁消除 :锁消除指的是 JVM 检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化 :锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操
作序列之外。
偏向锁 :当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程 ID ,之后 这个线程再次进入同步块时都不需要CAS 来加锁和解锁了,偏向锁会永远偏向第一
个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁 : JVM 的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM 将会使用
CAS 方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败, 当前线程就尝试自旋来获得锁。 整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。 简单点说,偏向锁就是通过对象头的偏向线程ID 来对比,甚至都不需要 CAS 了,而轻量级锁主要就 是通过CAS 修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
产生死锁的四个必要条件?
1. 互斥条件:一个资源每次只能被一个线程使用
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系