相比同步锁,JUC包中的锁的功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁,只是它的用法更难。
JUC包中的锁,包括:Lock接口,ReadWriteLock接口,LockSupport阻塞原语,Condition条件,AbstractOwnableSynchronizer、AbstractQueuedSynchronizer、AbstractQueuedLongSynchronizer三个抽象类,ReentrantLock独占锁,ReentrantReadWriteLock读写锁。由于CountDownLatch,CyclicBarrier和Semaphore也是通过AQS来实现的;文中也将它们归纳到锁的框架中进行介绍。
框架
先看看锁的框架图,如下所示。
Lock接口
JUC包中的Lock接口支持那些语义不同(重入、公平等)的锁规则。
所谓语义不同,是指锁可是有”公平机制的锁”、”非公平机制的锁”、”可重入的锁”等等。
“公平机制”是指”不同线程获取锁的机制是公平的”,
而”非公平机制”则是指”不同线程获取锁的机制是非公平的”,”可重入的锁”是指同一个锁能够被一个线程多次获取。
ReadWriteLock
接口以和Lock类似的方式定义了一些读取者可以共享而写入者独占的锁。JUC包只有一个类实现了该接口,即ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer就是被称之为AQS的类,它是一个非常有用的超类,可用来定义锁以及依赖于排队阻塞线程的其他同步器;
ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等这些类都是基于AQS类实现的。
AbstractQueuedLongSynchronizer类提供相同的功能但扩展了对同步状态的64位的支持。两者都扩展了类AbstractOwnableSynchronizer(一个帮助记录当前保持独占同步的线程的简单类)。
LockSupport
LockSupport提供“创建锁”和“其他同步类的基本线程阻塞原语”。
LockSupport的功能和”Thread中的Thread.suspend()和Thread.resume()有点类似”,LockSupport中的park() 和unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend和Thread.resume所可能引发的死锁”问题。
Condition
Condition需要和Lock联合使用,它的作用是代替Object监视器方法,可以通过await(),signal()来休眠/唤醒线程。
Condition接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用Object.wait访问的隐式监视器类似,但提供了更强大的功能。
需要特别指出的是,单个Lock可能与多个Condition对象关联。为了避免兼容性问题,Condition方法的名称与对应的Object版本中的不同。
ReentrantLock
ReentrantLock是独占锁。所谓独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
ReentrantLock锁包括”公平的ReentrantLock”和”非公平的ReentrantLock”。
“公平的ReentrantLock”是指”不同线程获取锁的机制是公平的”
“非公平的ReentrantLock”则是指”不同线程获取锁的机制是非公平的”,ReentrantLock是”可重入的锁”。
ReentrantReadWriteLock
ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括子类ReadLock和WriteLock。
ReadLock是共享锁,而WriteLock是独占锁。
CountDownLatch
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CyclicBarrier
CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。
CyclicBarrier和CountDownLatch的区别是:
- CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
- CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
Semaphore
Semaphore是一个计数信号量,它的本质是一个”共享锁”。
信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
基本概念
AQS – 指AbstractQueuedSynchronizer类。
AQS是java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现。AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semaphore)的公共父类。
AQS锁的类别 – 分为“独占锁”和“共享锁”两种。
独占锁
– 锁在一个时间点只能被一个线程锁占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁;而非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁。独占锁的典型实例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。共享锁
– 能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。这些锁的用途和原理,在以后的章节再详细介绍。
CLH队列 – Craig, Landin, and Hagersten lock queue
CLH队列是AQS中“等待锁”的线程队列。在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。
CLH是一个非阻塞的FIFO队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和CAS保证节点插入和移除的原子性。
CAS函数 – Compare And Swap
CAS函数,是比较并交换函数,是原子操作函数;即,通过CAS操作的数据都是以原子方式进行的。例如,compareAndSetHead(),compareAndSetTail(),等函数。它们共同的特点是,这些函数所执行的动作是以原子的方式进行的。
自旋锁、排队自旋锁、MCS锁、CLH锁
自旋锁(Spin lock)
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。
简单的实现
1 | import java.util.concurrent.atomic.AtomicReference; |
SimpleSpinLock里有一个owner属性持有锁当前拥有者的线程的引用,如果该引用为null,则表示锁未被占用,不为null则被占用。
这里用AtomicReference是为了使用它的原子性的compareAndSet方法(CAS操作),解决了多线程并发操作导致数据不一致的问题,确保其他线程可以看到锁的真实状态。
缺点
CAS操作需要硬件的配合;
保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重;
没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁。
Ticket Lock
Ticket Lock是为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。
当线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋。
简单的实现
1 | import java.util.concurrent.atomic.AtomicInteger; |
缺点
Ticket Lock虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
下面介绍的CLH锁和MCS锁都是为了解决这个问题的。
MCS来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。
CLH的发明人是:Craig,Landin and Hagersten。
MCS锁
MCS Spinlock是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
1 | public class MCSLock { |
CLH锁
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
1 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; |
CLH锁与 MCS锁的比较
下图是CLH锁和MCS锁队列图示:
差异:
从代码实现来看,CLH比MCS要简单得多。
从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
注意:这里实现的锁都是独占的,且不能重入的。