Java锁

JUC locks锁框架

JUC locks

早期的JDK版本中,仅仅提供了synchronizdwaitnotify等等比较底层的多线程同步工具。java.util.concurrent.locks包下提供了一系列基础的锁工具,用以对synchronizdwaitnotify等进行补充、增强。

1
2
3
4
5
6
7
8
interface Locks{
lock()
lockInterruptibly()
tryLock()
tryLock(long,TimeUtil)
unlock()
newCondition()
}

ReentrantLock类

ReentrantLock类,实现了Lock接口,是一种可重入的独占锁,它具有与使用 synchronized 相同的一些基本行为和语义,但功能更强大。ReentrantLock内部通过内部类实现了AQS(AbstractQueuedSynchronizer)框架的API来实现独占锁的功能。ReentrantLock默认使用非公平的策略,但是可以通过构造器添加参数来使用公平策略。

公平策略按照先来后到的原则来允许线程获得锁,避免了饥饿,但是受到线程唤醒的影响效率并不如非公平策略。实际上在非公平模式下,并发线程会在尝试获取锁的时候会首先通过CAS修改state,如果失败,则仍然进入队列排队,按照先来后到的顺序获取锁。公平模式下则是直接进入队列。

ReentrantLock中主要拥有一个内部类Sync,这是AQS的子类,也是ReentrantLock主要的实现原理。Sync的实例有两种,一种是FairSync另一种是NonfairSync,分别对应公平策略和非公平策略。对锁的获取最终会委托给Sync.tryAcquire(int)方法。这个方法是AQS中核心方法,留给子类实现。

在公平模式获取锁:

  1. 如果代表加锁次数的属性state为0,并且等待队列中没有其他线程,则以CAS的方式更新state。然后设置当前线程获得锁。
  2. 如果状态不为0,且当前线程已经获得了锁,则说明是重入情况。此时更新state,并处理可能会出现的溢出异常。
  3. 如果不是前两种情况,则获取锁失败了。此时通过自旋,将当前线程包装成一个节点,插入到等待线程的双向链表中。
  4. 从等待队列中取出当前节点并判断是否是首节点,并尝试获取锁,如果获取不到,则需要根据前置节点的状态判断是否阻塞当前线程。为了保证当前线程能够被唤醒,前置节点必须为SIGNAL。如果为CANCELLED,则说明前置节点应该被移除,并重新尝试。最终设置前置节点状态为SIGNAL然后阻塞当前线程。

释放锁:

  1. 当前线程必须是锁的独占线程,然后将state减1。如果state为0,则将独占线程设置为null
  2. 遍历队列,如果节点状态不为CANCELLED,则唤醒这个节点。
  3. 被唤醒节点会返回自己的中断标识,标识自己在阻塞期间是否被中断,这里是一种延迟中断策略。然后尝试获取锁。
  4. 获取锁之后会清楚其前置节点,并将自己设置为队首元素。

对于等待线程被中断时候,acquireQueued方法会用一个标志位用来记录,并不会抛出异常,而doAcquireInterruptibly方法则会直接抛出异常。

对于限时等待的支持,通过doAcquireNanos方法实现。这个方法是一个自旋操作,在规定时间内不断尝试获取锁,获取不到就阻塞,并且会响应中断。其阻塞是通过Unsafe.park方法调用native方法实现的。如果超时获取不到锁就会调用cancelAcquire方法取消一个正在获取锁的线程。该方法主要将到期的节点等待状态修改为CANCELLED,然后等待被GC回收。

LockSupport类

LockSupport类,是JUC包中的一个工具类,是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport类的核心方法其实就只有两个ParkUnpark,一个用于阻塞一个用于唤醒线程,相比于waitsignial方法,其能针对特定线程进行操作。

AbstractQueuedSynchronizer抽象类

AQS是整个JUC包的核心,提供了一套通用的机制来管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。ReentrantLockCountDownLatchCyclicBarrier等同步器,其实都是通过内部类实现了AQS框架暴露的API,以此实现各类同步器功能。这些同步器的主要区别其实就是对同步状态的定义不同。

AQS框架,分离了构建同步器时的一系列关注点,它的所有操作都围绕着资源——同步状态来展开,并替用户解决了如下问题:

  1. 资源是可以被同时访问?还是在同一时间只能被一个线程访问?(共享/独占功能)
  2. 访问资源的线程如何进行并发管理?(等待队列)
  3. 如果线程等不及资源了,如何从等待队列退出?(超时/中断)

这其实是一种典型的模板方法设计模式:父类(AQS框架)定义好骨架和内部操作细节,具体规则由子类去实现。

同步器 资源的定义
ReentrantLock 资源表示独占锁。State为0表示锁可用;为1表示被占用;为N表示重入的次数
CountDownLatch 资源表示倒数计数器。State为0表示计数器归零,所有线程都可以访问资源;为N表示计数器未归零,所有线程都需要阻塞。
Semaphore 资源表示信号量或者令牌。State≤0表示没有令牌可用,所有线程都需要阻塞;大于0表示由令牌可用,线程每获取一个令牌,State减1,线程每释放一个令牌,State加1。
ReentrantReadWriteLock 资源表示共享的读锁和独占的写锁。state逻辑上被分成两个16位的unsigned short,分别记录读锁被多少线程使用和写锁被重入的次数。

AQS提供了对并发管理的一套模板框架,开发者并不需要了解其中的实现细节,并且其中很多方法都是finalprivate的,设计者并不希望开发者使用这些方法。AQS暴露了一些待实现的API以方便开发者解决在并发中自定义的问题,包括资源是如何被定义如何方法的。

API 描述
tryAcquire() 排它获取(资源数)
tryRelease() 排它释放(资源数)
tryAcquireShared() 共享获取(资源数)
tryReleaseShared() 共享获取(资源数)
isHeldExclusively() 是否排它状态

在AQS内部使用了一个int类型的state代表了资源,并暴露出getState,setState,compareAndSetState方法来对state进行修改。对于线程的阻塞和唤醒,是通过LockSupport实现。对于等待线程的管理是AQS实现的核心,等待队列是一种严格FIFIO的队列,采用双向链表实现。基于一种CLS锁变种。CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

CLH队列中的节点都是对线程的封装,节点一共有两种类型分为独占节点(EXCLUSIVE)和共享节点(SHARED)。每个节点又会有一些自己的状态。CLH的实现中会用前一个节点的属性(waitStatus)来标识后一个节点的状态。当加入第一个线程时会默认创建一个虚拟节点,并使用虚拟节点的waitStatus标识第一个节点的状态。

状态 代码 含义
CANCELLED 1 取消。表示后驱结点被中断或超时,需要移出队列
SIGNAL -1 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
CONDITION -2 Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了
PROPAGATE -3 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
INITIAL 0 默认。新结点会处于这种状态

Condition类

AQS中包含了Condition的实现用来实现条件等待。Condition提供了await()方法将当前线程阻塞,并提供signal()方法支持另外一个线程将已经阻塞的线程唤醒。Condition需要结合Lock使用。线程调用await()方法前必须获取锁,调用await()方法时,将线程构造成节点加入等待队列,同时释放锁,并挂起当前线程。其他线程调用signal()方法前也必须获取锁,当执行signal()方法时将等待队列的节点移入到同步队列,当线程退出临界区释放锁的时候,唤醒同步队列的首个节点。Condition提供了类似object.waitnotify的线程通信机制,但是condition支持多个等待队列,使用上更加灵活。

Contiditon执行过程

CountDownLatch类

CountDownLatch采用AQS实现了共享锁的功能。其构造方法指定资源state。线程调用await()方法后会进入等待队列,并阻塞自身。当其他线程调用countDown()时会将资源数减少,等资源数位0时表示没有锁。此时CountDownLatch实现的tryReleaseShared方法会判断资源数是否为0,如果为0,则AQS的doReleaseShared方法被调用,此方法会唤醒等待队列的头节点,头节点被唤醒后会继续向后传播,直至唤醒所有节点。

CyclicBarrier与之有着类似功能,但是CyclicBarrier是可以重用的。CyclicBarrier通过ConditionReentrantLock来做实现。其关键代码在dowait方法中,当指定数量的线程全部到达时候如果指定了后续任务,则开始运行后续任务,否则先到达的线程阻塞等待后续线程运行。当阻塞线程被中断时,抛出BrokenBarrierException。通过reset方法来重置CyclicBarrier。该方法首先破坏栅栏,然后重置计数器。

Semaphore类

Semaphore内部构造和CountDownLatch大同小异,都是接用AQS的API,定义了公平策略和非公平策略两个内部类。其构造方法指定了许可数量和公平与否。当许可降低为0时候后续线程必须阻塞,和CountDownLatch逻辑有些相反。这其实反映了对资源状态不同的定义。

SyncSemaphore的一个内部抽象类,公平策略的FairSync和非公平策略的NonFairSync都继承该类。

Semaphore对于tryAcquireShared的实现主要是比较状态和剩余许可,当队列中有线程等待时,直接返回负数,代表失败。非公平策略则不管队列中是否有线程等待,直接尝试获取许可。否则CAS自旋将状态减少获取许可数,返回剩余状态。可以一次获取多个许可,如果剩余状态不足doAcquireSharedInterruptibly方法会将当前线程包装成Node节点,并加入等待队列。然后自旋尝试获取资源,如果获取失败则将前置节点设置为SINGNAL表示自身需要被唤醒,然后阻塞线程。

线程调用release方法归还许可,tryReleaseShared方法通过自旋CAS来更新state,直到更新成功。成功之后doReleaseShared方法会唤醒等待队列中的等待节点。被唤醒的节点继续原先的自旋,尝试获取许可,获得许可后将自身设置为等待队列的头节点,并传播状态尝试唤醒后续等待节点。

当资源大于1时候,Semaphore限制了并发量,但是并不能保证数据的一致性,它并不能起到锁的作用。

ReentrantReadWriteLock类

ReentrantReadWriteLock(以下简称RRW),也就是读写锁,是一个比较特殊的同步器,特殊之处在于其对同步状态State的定义与ReentrantLockCountDownLatch都很不同。

其构造方法仍然允许通过一个布尔值控制公平与否的策略。RRW提供了获取读锁写锁的方法,返回实例为两个实现了Lock接口的内部类。读锁是一种共享锁,实现了AQS的共享功能。其内部调用了acquireShared方法。

RRW仍然使用一个int类型的state表示资源的状态,但是在RRW里需要表示读状态和写状态,因此,int的高低16位分别表示两个状态。

读锁的获取过程:

  1. 如果写锁已被占用,并且不是当前线程独占,直接返回失败。
  2. 如果读锁重入数量超过65536,抛出异常。
  3. 如果根据公平性原则判断当前线程不需要等待并且CAS设置状态成功,返回成功。
  4. 如果没有返回,说明当前线程需要等待,或者CAS设置失败。此时调用fullTryAcquireShared一直自旋尝试CAS设置状态。

读锁的获取

写锁是一种独占锁,线程会在判断锁被其他线程获取的时候进入等待队列并被阻塞。
写锁的获取

读锁会通过内部类HoldCounter来保存每个线程的读锁重入数量。释放时,会将读锁重入数量减少1。当读锁重入数为0时,调用doReleaseShared方法来唤醒等待队列的线程。

在非公平模式下,写锁永远可以直接获取锁。这其实时性能优化的考虑,因为大多数情况写锁涉及的操作时间耗时要远大于读锁,频次远低于读锁,这样可以防止写线程一直处于饥饿状态。