1、说说你对 AQS 的理解
- AQS是多线程同步器,是JUC并发包下多个组件的底层实现,比如Lock,CountDownLatch,Semaphore都用到了 AQS;
- 从本质上来说,AQS提供了两种锁的机制,分别是排它锁和共享锁,所谓排它锁,就是存在多个线程去竞争同一个共享资源的时候,同一时刻,只允许一个线程去访问这样一个共享资源,也就是说,多个线程中,只能有一个线程去获得这样一个锁资源,比如 Lock 中的 ReenTrantLock 重入锁,它的一个实现就是用到了AQS中的一个排它锁的功能,共享锁呢,也称为读锁,就是在同一时刻,允许多个线程同时获得这样一个锁的资源,比如 CountDownLatch 以及 Semaphore 都用到了 AQS 中的共享锁功能;
- 那么AQS作为互斥锁来说呢,他的整个设计体系中需要解决三个核心问题,首先是互斥变量的设计以及如何保证多线程同时更新互斥变量的时候线程的安全性,其次是未获取到锁资源的线程的等待以及竞争到锁资源释放锁之后的唤醒,最后是锁的竞争的公平性和非公平性。
- AQS采用了一个int类型的互斥变量 state 用来记录锁竞争的一个状态,0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有现成正在持有锁资源,一个线程来获取锁资源的时候首先会判断 state 是否等于0,也就是说,如果它是无锁状态,如果是,则把这个 state 更新成1,表示占用到锁,而这个过程中,如果有多个线程同时去做这样一个操作,就会导致线程安全问题,因此AQS采用了CAS机制去保证 state 互斥变量更新的一个原子性。
- 未获得到锁的线程,通过 Unsafe 类中的 park 方法去进行阻塞,把阻塞的线程按照先进先出的原则去加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从这个双向链表的头部去唤醒下一个等待的线程再去竞争锁。
- 最后关于锁竞争的公平性和飞公平性的问题AQS的处理方式是在竞争锁的时候公平锁需要去判断双向链表中是否有阻塞的线程,如果有呢,则需要去排队等待,非公平锁的处理方式是不管双向链表中是否存在等待竞争锁的线程,它都会直接去尝试更改互斥变量 state 去竞争锁,假设在一个临界点,获得锁的线程释放锁,此时 state 等于0,而当前的这个线程去抢占锁的时候正好可以把state修改成1,那么这个时候,就表示它可以拿到锁资源,这个过程是非公平的。
2、lock和synchronized的区别
- 从功能角度来说 lock 和 synchronized 都是Java中去用来解决线程安全的工具;
- 从特性来看,synchronized 是Java中的同步关键字,lock 是 JUC包里面提供的一个接口,而这个接口,它有很多的实现类,其中就包括 ReentrantLock 这样一个重入锁的实现,synchronized 可以通过两种方式去控制锁的粒度,修饰在方法上和修饰在代码块上,并且我们可以通过 synchronized 加锁对象的生命周期来控制锁的作用范围,比如锁对象是静态对象或类对象,那么这个锁就属于全局锁,如果锁对象是普通实例对象,那么这个锁的范围取决于这个实例的生命周期。lock中锁的粒度是通过它里面提供的lock()方法和unlock()方法来决定的,包裹在两个方法之间的代码是能够保证线程安全的,而锁的作用域取决于lock实例的生命周期;
- lock比synchronized的灵活性更高,lock可以自主的去决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()这两个方法就可以了,同时,lock还提供了非阻塞竞争锁的方法,叫trylock(),这个方法可以通过返回true和false来告诉当前线程是否已经有其他线程正在使用锁,而synchronized由于是关键字,所以他无法去实现非阻塞竞争锁的方法,另外synchronized锁的释放是被动的,就是当synchronized同步代码块执行结束以后,或者代码出现异常的时候才会被释放;
- lock提供了公平锁和非公平锁的机制,公平锁时指线程竞争锁资源的时候,如果已经有其他线程正在排队或者等待锁释放,那么当前竞争锁的线程是无法插队的,而非公平锁就是不管是否有线程正在排队等待锁,它都会去尝试竞争一次锁,synchronized只提供了一种非公平锁的实现;
- 从性能方面来说synchronized和lock在性能方面相差不大,在实现上会有一定的区别,synchronized引入了偏向锁、轻量级锁、重量级锁以及锁升级的机制来实现锁的优化,而lock中呢,则用到了自旋锁的方式去实现性能优化
3、ConcurrentHashMap的底层实现原理
- ConcurrentHashMap的整体架构:它是由数组、单向链表、红黑树来构成的,初始化一个ConcurrentHashMap的实例的时候,默认会初始化一个长度等于16的数组。由于ConcurrentHashMap的核心仍然是哈希表,所以必然会存在Hash冲突的问题,所以ConcurrentHashMap采用链式寻址的方式来解决哈希冲突,当哈希冲突比较多的时候,会造成链表长度较长的问题,这种情况下会使得ConcurrentHashMap中的一个数组元素查询复杂度会增加,所以在Java8中引入了红黑树的机制,当数组长度大于64,且链表长度大于等于8的时候,单向链表就会转换成红黑树,另外,随着ConcurrentHashMap的动态扩容,一旦链表的长度小于8,红黑树会退化成单向链表;
- ConcurrentHashMap的基本功能:它本质上是一盒HashMap,因此功能和HashMap是一样的,但是ConcurrentHashMap在HashMap的基础上提供了并发安全的一个实现,并发安全的主要实现主要是对Node节点去加锁来保证数据更新的安全性
- ConcurrentHashMap在性能方面的优化:如何在并发性能和数据安全去做好平衡在很多地方都有类似的设计,比如像cpu的三级缓存、mysql的bufferpool、synchronized的锁升级等,ConcurrentHashMap也做了类似的优化设计,在Java8里面,ConcurrentHashMap的锁的粒度是数组中的某一个节点,在Java7里面,锁定的是segment,锁的范围要更大,所以性能上它会更低,其次,引入红黑树这样的机制,去降低了数据查询的时间复杂度,红黑树的时间复杂度是ologn,第三,当数组的长度不够的时候,ConcurrentHashMap需要对数组进行扩容,而在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的一个实现,简单来说,就是多个线程对原始数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而去整体的提升了扩容过程中数据迁移的效率,ConcurrentHashMap有一个size方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下去实现元素个数的累加性能是非常低的,所以ConcurrentHashMap在这个方面做了两个点的优化,第一个点是当线程竞争不激烈的时候直接采用CAS的方式来实现元素个数的一个原子递增,第二个点如果线程竞争比较激烈的情况下使用一个数组来维护元素个数,如果要增加总的元素个数的时候,直接从数组中随机选择一个,在通过CAS来实现原子递增,核心思想是引入了数组来实现对并发更新的一个负载