Lock锁机制详解(一)
在详细探讨Lock锁机制之前,首先来思考一个问题:为什么有了Synchronized还要提供Lock接口?
1、Lock锁机制存在的原因
1) 死锁方面
虽然Java对synchronized
做了许多优化,大大提升了锁的性能,但是由于Synchronized锁是无法主动释放锁资源的,因此会涉及到死锁的问题。
死锁发生的四个必要条件如下:
- 互斥
- 不可剥夺
- 请求与保持
- 循环等待
问题的关键在于Synchronized
锁无法主动释放资源,但在大多数场景下,我们都希望不可剥夺这一条件能够被破坏,如果占用资源的线程申请不到其他资源,就可以主动释放自己占有的资源,因此Java提供了Lock
包下的锁对象供开发者使用。
那么Lock
包下的锁是如何解决这一问题的呢?其实主要是通过三方面来进行设计:响应中断、支持超时和非阻塞获锁
- 响应中断(
void lockInterruptibly() throws InterruptedException;
方法)
线程使用synchronized
锁,如果线程阻塞并且进入死锁状态,就没有机会来唤醒阻塞的线程。Lock锁提供了响应中断的机制,使得阻塞态的线程能够响应中断信号而被唤醒,从而有机会释放已占有的资源来破坏不可抢占的条件。
- 支持超时(
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
)
如果线程在一段时间内没有获取到锁,线程不会进入阻塞态,而是返回一个错误。通过该机制,线程也有机会去释放持有的锁,破坏不可剥夺的条件。
- 非阻塞(
boolean tryLock();
)
线程使用该方法尝试获取锁,获取成功返回true,失败不会进行等待,而是直接返回false,同样使得线程可以释放锁。
总的来说,Lock锁机制通过破坏不可剥夺的条件,从而在一定程度上避免了死锁问题。
2)使用特性
Lock
接是 JUC 包的顶层接口,基于Lock
接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性束缚,在类库层面实现同步。
在使用方面,Lock包增加的高级功能有:
- 等待可中断: 持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。
- 公平锁: 公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何线程都有机会获得锁。
synchronized
是非公平的,ReentrantLock
在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会急剧下降,影响吞吐量。 - 锁绑定多个条件: 一个
Lock
锁可以同时绑定多个Condition
。synchronized
中锁对象的wait
跟notify
可以实现一个隐含条件,如果要和多个条件关联就不得不额外添加锁,而Lock
锁可以多次调用newCondition
创建多个条件。
因此,在使用方面,Lock锁提供了一些更灵活的方法去控制同步动作。
3)应用场景
Lock锁机制提供了各式各样的锁供我们使用,每种锁可以适配不同的场景,例如:
ReentrantLock
:与synchronized
类似,但可以指定是公平锁还是非公平锁。ReadWriteLock
:读写锁,包含读共享锁和写互斥锁,在读多写少的场景下可以大大提升读写性能。Semaphore
:信号量,用于指定线程同步的个数CountDownLatch
:计数器(一次性),用于线程之间的等待执行(类比join()
方法)CyclicBarrier
:回环屏障,可以满足计算器的重置操作。(与计数器有所区别,之后会讨论)
通过这些不同种类的锁实现,可以在不同场景下灵活使用。
2、锁实现原理
JUC的锁框架如下:
从类图中可以看到,锁框架中锁的实现都是围绕一个最底层的数据结构来实现的,也就是AbstractQueuedSynchronizer
抽象同步队列,该抽象类的结构如下:
1)组成
AQS是一个FIFO的双向队列,其中包含以下几个关键字段
-
head
、tail
记录队首和队尾元素,元素类型为Node
-
静态内部类
Node
,该类用于包装线程的同步信息thread
:存放目标线程SHARED/EXCLUSIVE
:标记线程是获取独占还是共享资源时被挂起的waitStatus
:记录当前线程的等待状态(CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点)next
:记录当前节点的后继节点pre
:记录当前节点的前驱结点 -
同步状态信息
state
,这是实现锁机制的关键变量,该字段使用volatile
修饰,可以通过getState/setState/compareAndSetState
等方法修改其值state的值在不同的锁中有不同的含义,比如在
ReentrantLock
中表示可重入次数;在ReadWriteLock
中前后16位表示读写锁的重入次数;在Semaphore
中表示信号量的个数;在CountdownLatch
中表示计数器的值。 -
内部类
ConditionObject
,该类用于结合锁来实现线程同步,称为条件变量。每一个条件变量对应一个条件队列(单向链表),用于存放调用条件变量的await
后被阻塞的线程 -
工具类
LockSupport
,该类的主要作用是挂起和唤醒线程,AQS的底层方法实现会使用该工具类的park/unpark
相关方法去进行线程阻塞唤醒操作,如下面的unparkSuccessor
方法
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//唤醒后续线程
LockSupport.unpark(s.thread);
}
LockSupport
类与每个使用它的线程都会关联一个许可证,默认情况下线程不持有许可证。
- 如果线程中调用了
park
方法并且该线程有许可证,方法会立刻返回;反之,线程不持有许可证,当前线程会被阻塞挂起unpark(Thread thread)
方法会为目标线程分配一个许可证,如果线程之前被park
阻塞,会唤醒该线程注意:如果其他线程调用了阻塞线程的
interrupt
方法,设置了中断标志或线程被虚假唤醒,该阻塞线程也会被唤醒。
2)操作
对AQS来说,线程同步的关键是对状态值state
进行操作,根据state
是否属于一个线程,操作state的方式分为独占方式和共享方式。
独占方式获取和释放资源的方法为:
acquire(int arg)
(不响应中断)void acquireInterruptibly(int arg)
,该方法可响应中断(抛异常)boolean release(int arg)
共享方式下相关方法为:
acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)
下面以独占方式为例,介绍获取与释放锁的步骤。
》获锁:acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当一个线程调用acquire
方法获取独占资源时,会首先使用tryAcquire(int arg)
方法获取资源,该方法实际上是一个空方法(如下),交由具体的子类去实现。
protected boolean tryAcquire(int arg) {
//直接报错,不支持的操作
throw new UnsupportedOperationException();
}
tryAcquire()
方法的具体实现留到后面进行说明,这里只要知道其本质就是去设置state
变量的值,如果修改成功该方法会直接返回true,因此acquire
方法直接返回。
否则可以看到调用了acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法,其中
-
addWaiter(Node.EXCLUSIVE)
方法将当前线程封装成一个类型为Node.EXCLUSIVE
的Node
节点,然后插入到AQS阻塞队列的尾部private Node addWaiter(Node mode) { //以传入类型来构造新节点 Node node = new Node(mode); //CAS循环设置该节点到阻塞队列尾部 for (;;) { Node oldTail = tail; if (oldTail != null) { node.setPrevRelaxed(oldTail); //CAS设置节点到队尾,成功则返回 if (compareAndSetTail(oldTail, node)) { oldTail.next = node; return node; } } else { //队列为空先进行初始化 initializeSyncQueue(); } } }
-
acquireQueued
方法实现如下:final boolean acquireQueued(final Node node, int arg) { //设置中断标志(不允许中断) boolean interrupted = false; try { for (;;) { //获取前驱节点 final Node p = node.predecessor(); //前驱节点为头节点,会再次尝试获取锁资源,成功把当前节点设置成新的头节点,返回中断标志 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return interrupted; } //请求失败后调用,根据节点的不同状态进行相应的操作 if (shouldParkAfterFailedAcquire(p, node)) //会调用park方法阻塞线程,捕捉中断信号,但不响应(抛异常) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { //如果异常会取消该节点的同步动作,如果发生过中断会进行中断处理 cancelAcquire(node); if (interrupted) selfInterrupt(); throw t; } }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ //当前节点是signal状态,返回true,表明需要阻塞并等待后续唤醒 return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ //如果当前节点的前驱节点是取消同步的节点,需要找到合法的前驱节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); //将当前节点连接到合法的前驱后面 pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ //否则(要么没设置状态;要么要进行唤醒传播)重新设置状态 pred.compareAndSetWaitStatus(ws, Node.SIGNAL); } //返回false,表明需要重新争用资源 return false;
private final boolean parkAndCheckInterrupt() { //阻塞线程 LockSupport.park(this); return Thread.interrupted(); }
总流程图如下:
》释放锁:release()
public final boolean release(int arg) {
//tryRelease释放资源
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
与获锁时类似,当一个线程调用release
方法时,会首先使用tryRelease(int arg)
方法释放资源,该方法也是一个空方法(如下),交由具体的子类去实现。其本质还是通过CAS设置state
变量的值。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//先修改节点的状态为初始状态
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//如果后继节点不合法(不需要被唤醒),那么从队尾往前遍历找到合法的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//唤醒后继节点
LockSupport.unpark(s.thread);
}
在成功修改state
值后,会调用unparkSuccessor(thread)
方法激活AQS队列里面被阻塞的一个线程,这个被激活的线程会使用tryAcquire(arg)
方法再次尝试获取资源(就是上面aquireQueue
中获锁的内容)。
- 共享方式下的资源获取释放流程与上面类似,这里不做讨论
- 使用可响应中断方法获取资源的方式会增加响应中断的逻辑(有中断会抛异常),有兴趣可以去自行研究
》条件变量的使用
首先通过一个简单的例子来理解Condition
的使用
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
public static void main(String[] args) {
//创建一个独占锁
ReentrantLock lock = new ReentrantLock();
//创建一个条件变量
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try{
System.out.println("已上锁,加入条件队列等待");
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
//挂起当前线程(类似于Object.wait()方法,只能在获取到锁后调用)
condition.await();
System.out.println("等待结束");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
new Thread(() -> {
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
lock.lock();
try{
System.out.println("已上锁,唤醒条件队列中的线程");
//唤醒挂起的线程(类似于Object.notify()方法,只能在获取到锁后调用)
condition.signal();
System.out.println("唤醒完毕");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
}
}
可以看出,类似于配合synchronized
使用的wait
和notify
,Condition
条件变量也可以配合Lock锁来实现线程间的同步动作。但两者的不同之处在于AQS的锁可以对应多个条件变量进行同步控制。
接下来我们看看条件变量的await()
方法和signal()
方法具体做了那些工作
public final void await() throws InterruptedException {
//有中断抛异常
if (Thread.interrupted())
throw new InterruptedException();
//创建新的node节点,插到条件队列尾部
Node node = addConditionWaiter();
//释放当前线程获取的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果线程处于正常状态,调用park方法挂起线程
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//跳出上面的循环(被唤醒)会调用acquireQueued方法进行当前节点在AQS队列中的动作(signal方法会唤醒线程并加入AQS队列)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled 删除取消同步的节点
unlinkCancelledWaiters();
if (interruptMode != 0) //有中断处理中断
reportInterruptAfterWait(interruptMode);
}
可以看出,调用await
会在内部构造一个类型为Node.CONDITION
的node
节点,并加入条件队列末尾,然后释放锁进行阻塞,等待后续被唤醒。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取队头节点
Node first = firstWaiter;
if (first != null)
//将队头节点移到AQS队列
doSignal(first);
}
private void doSignal(Node first) {
do {
//修改条件队列的队头队尾
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//已经取消同步的节点不做处理
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
//enq方法将节点加入AQS队尾
Node p = enq(node);
int ws = p.waitStatus;
//设置该节点的状态,并唤醒节点
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
可以看出,signal()
方法会将条件队列队头节点移入AQS,并且修改状态,唤醒该线程以进行后续动作(await()
方法里while
语句后面的逻辑)。
signalAll()
方法与signal
类似,只不过针对的是条件队列中的所有节点。
最后做一个小结:
一个锁对应一个
AQS
阻塞队列,当线程拿不到锁时会加入队尾调用park
方法阻塞自己,直到被唤醒,再调用tryAcquire
方法获取锁。一个锁可对应多个条件变量,每个条件变量维护自己的一个队列,当一个线程调用了条件变量的await
方法,便会把当前线程加入队列进行等待,并释放锁。当其他线程调用了该条件变量的sign
方法,会唤醒条件队列中的队头元素,加入AQS
中等待获取锁。
3、实现自定义锁
了解了锁实现的原理之后,我们尝试基于AQS自定义一个同步锁,深化我们的理解。
自定义不可重入排它锁NonReetrantLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class NonReentrantLock implements Lock, java.io.Serializable{
//定义一个内部辅助类
private static class Sync extends AbstractQueuedSynchronizer{
//判断锁是否被获取
protected boolean isHeldExclusively(){
//state为1表明有其他线程已经占用了锁
return getState() == 1;
}
//尝试获取锁
protected boolean tryAcquire(int acquires){
assert acquires == 1;//判断是否真的有请求
if(compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//尝试释放锁
protected boolean tryRelease(int releases){
assert releases == 1;
if(getState() == 0){//若state为0,表明锁没有被占有,报错
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//提供条件变量的生成接口
Condition newCondition(){
return new ConditionObject();
}
}
//创建一个辅助类进行具体工作
private final Sync sync = new Sync();
/*
* 下面是锁提供的一些外部接口
* */
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLock(){
return sync.isHeldExclusively();
}
}
测试类:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
public class NonReentrantLockTest {
public static void main(String[] args) {
NonReentrantLock lock = new NonReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try{
if(lock.isLock()){
System.out.println("已上锁,加入条件队列等待");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.await();
System.out.println("等待结束");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
if(lock.tryLock(1, TimeUnit.SECONDS)){
System.out.println("已上锁,唤醒条件队列中的线程");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
condition.signal();
System.out.println("唤醒完毕");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
}
}
执行结果:
小结
本篇文章详细探讨了Lock包下的锁底层机制,主要从锁的组成、锁的底层操作逻辑两个方面进行了讨论,但是没有涉及具体的锁实现,后续将对该包下的各种锁实现进行分析。