Lock
Java中的Lock是J.U.C中的一个核心组件,J.U.C(java.util.concurrent)是在并发编程中常用的工具类,里面包含了很多在并发场景中使用的组件,比如线程池、阻塞队列、计时器、同步器、并发集合等。 在J.U.C中的大部分组件中都用到了Lock,在Lock出现之前,只能使用synchronized来处理并发编程中的线程安全问题。但是synchronized不是特别灵活,它是基于jvm层面的锁,锁的释放由jvm自行完成,这样虽然方便了,但是增加了不可控的问题,不够灵活,锁的升级也很难人为控制,另外,synchronized无法实现公平锁。为了弥补synchronized的短处,Java5以后出现了Lock。 Lock其实本质上是一个接口,它定义了锁的获取和释放的抽象方法,形成了锁的一个规范,意味着可以实现不同的锁,比如ReentrantLock、ReentrantReadWriteLock、StampedLock。
ReentrantLock
顾名思义,ReentrantLock就是可重入锁(synchronized也是可重入锁),它是唯一一个实现了Lock接口的类,重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数,释放锁时计数器减一,计数器为0则表示锁没有被线程持有。
ReentrantReadWriteLock
重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock
stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。
ReentrantLock
可重入锁,设计的目的就是为了避免线程的死锁。 ReentrantLock的简单实用方法:
public class StudyReentrantLock {
static Lock lock = new ReentrantLock ( ) ;
public static void main ( String[ ] args) {
lock. lock ( ) ;
try {
TimeUnit. SECONDS. sleep ( 1 ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
lock. unlock ( ) ;
}
}
}
ReentrantLock实现原理
锁的基本原理是,将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?
AQS
AQS(AbstractQueuedSynchronizer),是一个同步队列,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。 从使用层面来说,AQS 的功能分为两种:独占和共享。独占锁,每次只能有一个线程持有锁,比如ReentrantLock 就是以独占方式实现的互斥锁;共 享 锁 ,允 许多个线程同时获取锁 ,并发访问共享资 源 ,比 如ReentrantReadWriteLock。
AQS内部实现
AQS 队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。下面是源码:
static final class Node {
static final Node SHARED = new Node ( ) ;
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1 ;
static final int SIGNAL = - 1 ;
static final int CONDITION = - 2 ;
static final int PROPAGATE = - 3 ;
volatile int waitStatus;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared ( ) {
return nextWaiter == SHARED;
}
final Node predecessor ( ) throws NullPointerException {
Node p = prev;
if ( p == null)
throw new NullPointerException ( ) ;
else
return p;
}
Node ( ) {
}
Node ( Thread thread, Node mode) {
this . nextWaiter = mode;
this . thread = thread;
}
Node ( Thread thread, int waitStatus) {
this . waitStatus = waitStatus;
this . thread = thread;
}
}
其中,设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head节点设置为原首节点的后继节点,并且断开原head节点的next引用即可。
线程同步
首先找到ReentrantLock 获取锁的入口,lock()方法
public void lock ( ) {
sync. lock ( ) ;
}
接下来看sync sync是一个静态的抽象内部类,继承了AbstractQueuedSynchronizer来实现重入锁的逻辑,我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。 Sync 有两个具体的实现类,分别是:NofairSync,表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁;FairSync,表示所有线程严格按照 FIFO 来获取锁。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691 L;
final void lock ( ) {
if ( compareAndSetState ( 0 , 1 ) )
setExclusiveOwnerThread ( Thread. currentThread ( ) ) ;
else
acquire ( 1 ) ;
}
protected final boolean tryAcquire ( int acquires) {
return nonfairTryAcquire ( acquires) ;
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = - 3000897897090466540 L;
final void lock ( ) {
acquire ( 1 ) ;
}
protected final boolean tryAcquire ( int acquires) {
final Thread current = Thread. currentThread ( ) ;
int c = getState ( ) ;
if ( c == 0 ) {
if ( ! hasQueuedPredecessors ( ) &&
compareAndSetState ( 0 , acquires) ) {
setExclusiveOwnerThread ( current) ;
return true ;
}
}
else if ( current == getExclusiveOwnerThread ( ) ) {
int nextc = c + acquires;
if ( nextc < 0 )
throw new Error ( "Maximum lock count exceeded" ) ;
setState ( nextc) ;
return true ;
}
return false ;
}
}
从以上代码可以初步看出,ReentrantLock的公平锁和非公平锁的实现逻辑,非公平锁的情况下,线程在抢占锁时会先通过CAS抢占锁,失败则调用acquire(1)走锁竞争逻辑;公平锁则是在线程抢占锁时,直接调用acquire(1)走锁竞争逻辑。 CAS 的实现原理
protected final boolean compareAndSetState ( int expect, int update) {
return unsafe. compareAndSwapInt ( this , stateOffset, expect, update) ;
}
通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false.这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示:1. 当 state=0 时,表示无锁状态;2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁。 另外,Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些底层操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障。而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false; stateOffset:一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量。 继续看acquire(1)
public final void acquire ( int arg) {
if ( ! tryAcquire ( arg) &&
acquireQueued ( addWaiter ( Node. EXCLUSIVE) , arg) )
selfInterrupt ( ) ;
}
这个方法的主要逻辑是:1. 通过 tryAcquire 尝试获取独占锁,如果成功返回true,失败返回 false;2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部;3. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。 tryAcquire 最终调用的是nonfairTryAcquire(以非公平锁为例):
final boolean nonfairTryAcquire ( int acquires) {
final Thread current = Thread. currentThread ( ) ;
int c = getState ( ) ;
if ( c == 0 ) {
if ( compareAndSetState ( 0 , acquires) ) {
setExclusiveOwnerThread ( current) ;
return true ;
}
}
else if ( current == getExclusiveOwnerThread ( ) ) {
int nextc = c + acquires;
if ( nextc < 0 )
throw new Error ( "Maximum lock count exceeded" ) ;
setState ( nextc) ;
return true ;
}
return false ;
}
这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false。 当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node。入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能:1. 将当前线程封装成 Node;2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列;3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列。
private Node addWaiter ( Node mode) {
Node node = new Node ( Thread. currentThread ( ) , mode) ;
Node pred = tail;
if ( pred != null) {
node. prev = pred;
if ( compareAndSetTail ( pred, node) ) {
pred. next = node;
return node;
}
}
enq ( node) ;
return node;
}
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁:1. 获取当前节点的 prev 节点;2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁;3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点;4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程;5. 最后,通过 cancelAcquire 取消获得锁的操作。
final boolean acquireQueued ( final Node node, int arg) {
boolean failed = true ;
try {
boolean interrupted = false ;
for ( ; ; ) {
final Node p = node. predecessor ( ) ;
if ( p == head && tryAcquire ( arg) ) {
setHead ( node) ;
p. next = null;
failed = false ;
return interrupted;
}
if ( shouldParkAfterFailedAcquire ( p, node) &&
parkAndCheckInterrupt ( ) )
interrupted = true ;
} finally {
if ( failed)
cancelAcquire ( node) ;
}
}
线程的挂起使用的是LockSupport.park()
private final boolean parkAndCheckInterrupt ( ) {
LockSupport. park ( this ) ;
return Thread. interrupted ( ) ;
}
LockSupport类是 Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积。
锁的释放
在 unlock 中,会调用 release 方法来释放锁
public void unlock ( ) {
sync. release ( 1 ) ;
}
public final boolean release ( int arg) {
if ( tryRelease ( arg) ) {
Node h = head;
if ( h != null && h. waitStatus != 0 )
unparkSuccessor ( h) ;
return true ;
}
return false ;
}
tryRelease方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将Owner 线程设置为空,而且也只有这种情况下才会返回 true。
protected final boolean tryRelease ( int releases) {
int c = getState ( ) - releases;
if ( Thread. currentThread ( ) != getExclusiveOwnerThread ( ) )
throw new IllegalMonitorStateException ( ) ;
boolean free = false ;
if ( c == 0 ) {
free = true ;
setExclusiveOwnerThread ( null) ;
}
setState ( c) ;
return free;
}
private void unparkSuccessor ( Node node) {
int ws = node. waitStatus;
if ( ws < 0 )
compareAndSetWaitStatus ( node, ws, 0 ) ;
Node s = node. next;
if ( s == null || s. waitStatus > 0 ) {
s = null;
for ( Node t = tail; t != null && t != node; t = t. prev)
if ( t. waitStatus <= 0 )
s = t;
}
if ( s != null)
LockSupport. unpark ( s. thread) ;
}
Condition
synchronized可以基于wait/notify实现线程间通信,Lock同样可以基于Condition来实现线程间的通信功能。
public class StudyReentrantLock {
static Lock lock = new ReentrantLock ( ) ;
static Condition condition = lock. newCondition ( ) ;
public static void main ( String[ ] args) throws InterruptedException {
test1 ( ) ;
TimeUnit. SECONDS. sleep ( 1 ) ;
test2 ( ) ;
}
public static void test1 ( ) {
new Thread ( ( ) - > {
lock. lock ( ) ;
try {
System. out. println ( Thread. currentThread ( ) . getName ( ) + "获得锁并执行等待" ) ;
condition. await ( ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
lock. unlock ( ) ;
System. out. println ( Thread. currentThread ( ) . getName ( ) + "执行完成" ) ;
}
} , "test1" ) . start ( ) ;
}
public static void test2 ( ) {
new Thread ( ( ) - > {
lock. lock ( ) ;
try {
System. out. println ( Thread. currentThread ( ) . getName ( ) + "获得锁并唤醒等待的线程" ) ;
condition. signal ( ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
lock. unlock ( ) ;
System. out. println ( Thread. currentThread ( ) . getName ( ) + "执行完成" ) ;
}
} , "test2" ) . start ( ) ;
}
}
await
通过这个案例简单实现了 wait 和 notify 的功能,当调用 await 方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以,condition 中两个最重要的方法,一个是 await,一个是 signal 方法:await:把当前线程阻塞挂起;signal:唤醒阻塞的线程。 调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁。
public final void await ( ) throws InterruptedException {
if ( Thread. interrupted ( ) )
throw new InterruptedException ( ) ;
Node node = addConditionWaiter ( ) ;
int savedState = fullyRelease ( node) ;
int interruptMode = 0 ;
while ( ! isOnSyncQueue ( node) ) {
LockSupport. park ( this ) ;
if ( ( interruptMode = checkInterruptWhileWaiting ( node) ) != 0 )
break ;
}
if ( acquireQueued ( node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if ( node. nextWaiter != null)
unlinkCancelledWaiters ( ) ;
if ( interruptMode != 0 )
reportInterruptAfterWait ( interruptMode) ;
}
signal
调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
public final void signal ( ) {
if ( ! isHeldExclusively ( ) )
throw new IllegalMonitorStateException ( ) ;
Node first = firstWaiter;
if ( first != null)
doSignal ( first) ;
}
private void doSignal ( Node first) {
do {
if ( ( firstWaiter = first. nextWaiter) == null)
lastWaiter = null;
first. nextWaiter = null;
} while ( ! transferForSignal ( first) &&
( first = firstWaiter) != null) ;
}
该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒
Condition 总结
阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁 释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入正常锁的获取流程