一、Random与ThreadLocalRandom
每个Random实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以ThreadLocalRandom应运而生。
ThreadLocalRandom使用ThreadLocal的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。
二、AtomicLong和LongAdder
在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,但是这些都是阻塞算法,对性能有一定损耗。而AtomicLong这些原子操作类都使用CAS非阻塞算法,性能更好,但是在高并发情况下AtomicLong还会存在性能问题。
LongAdder原子性操作类,该类通过内部cells数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对cells数组里面的元素进行并行的更新操作。另外,数组元素Cell使用@sun.misc.Contended注解进行修饰,这避免了cells数组内多个原子变量被放入同一个缓存行,也就是避免了为共享,这对性能也是一个提升。
三、CopyOnWriteArrayList
CopyOnWriteArrayList使用写时复制的策略老保证list的一致性,而获取——修改——写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改。另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。
四、抽象同步队列AQS
AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。
图6-1是AQS的类图。由该图可知,AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGETE(释放共享资源时需要通知其他节点);prev记录当前节点的前驱节点,next记录当前节点的后续节点。
在AQS中维持来了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数器当前的值。
AQS有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如类图所示,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。
在线程中,notify和wait,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。
他们的不同在于,synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。
在调用共享变量的notify和wait方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的signal和await方法前也必须先获取条件变量对应的锁。
最后使用一个图总结如下:一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
五、ReentrantLock
如图6-5所示,假如线程Thread1、Thread2和Thread3同时尝试获取独占锁ReentrantLock,假设Thread1获取到了,则Thread2和Thread3就会被转换为Node节点并被放入ReentrantLock对应的AQS阻塞队列,而后被阻塞挂起。
如图6-6所示,假设Thread1获取锁后调用了对应的锁创建的条件变量1,那么Thread1就会释放获取到的锁,然后当前线程就会被装换为Node节点插入条件变量1的条件队列。由于Thread1释放了锁,所以阻塞到AQS队列里面的Thread2和Thread3就会有机会获取到该锁,假如使用的是公平策略,那么这时候Thread2会获取到该锁,从而从AQS队列里面移除Thread2对应的Node节点。
六、ReentrantReadWriteLock
ReentrantReadWriteLock的底层是使用AQS实现的。ReentrantReadWriteLock巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现了读写分离,这在读多写少的场景下比较适用。
七、StampedLock
StampedLock提供了三种读写模式的锁
1.写锁 writeLock
2.悲观读锁 readLock
3.乐观读锁 tryOptimisticRead
StampedLock提供的读写锁与ReentrantReadWriteLock类似,只是前者提供的是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供了更好的性能,这时英文获取乐观读锁时不需要进行CAS操作设置锁的状态,而只是简单地测试状态。