目录
一、Lock接口
1. Lock使用
并发包下java.util.concurrent.locks.Lock接口实现锁的功能,必须显示地获取和释放锁。如下图所示,是Lock的类图。
如下代码是Lock的使用实例。注意lock.lock()必须在try块之外,否在获取锁失败出现异常,会导致锁无故释放。
// 重入锁
Lock lock = new ReentrantLock();
@Test
public void lockUseTest(){
lock.lock();
try {
// 业务逻辑处理
System.out.println("test use lock");
} finally {
lock.unlock();
}
}
2. synchronized与Lock对比
内容 | synchronized | Lock |
层次 | 关键字 | 接口 |
实现 | 隐式地释放和获取锁 | 显示地释放和获取锁 |
释放锁 | 线程正常的执行完、JVM抛出异常 | finally块释放锁,否则易死锁 |
获取锁 | 只有释放锁才能获取锁 | 视情况,共享锁/排他锁 |
锁状态 | 无法判定 | 可以判定 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
3. Lock的API
Lock接口定义了锁获取和释放的基本操作,而获取和释放锁的实现原理都是队列同步器java.util.concurrent.locks.AbstractQueuedSynchronizer及其子类完成。
二、AbstractQueuedSynchronizer
队列同步器(java.util.concurrent.locks.AbstractQueuedSynchronizer)用来实现锁的获取和释放,底层使用了一个int型的变量state来表示同步状态(private volatile int state),通过内置的FIFO双向队列完成线程的排队工作。
同步器的使用主要是继承,子类继承它的抽象方法来管理同步状态state。其主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等同步器的核心结构。
1. 同步队列
如上图所示,同步队列的基本结构是一个FIFO双向队列;同步失败的线程信息构造成Node节点。实现机制:获取同步状态失败(即:更新state值失败)时,线程进入等待状态,构造成Node节点加入到同步队列的尾部,tail节点指向尾部节点;当同步状态释放时,首节点的后继节点线程被唤醒,使其再次尝试获取同步状态。
如上图所示,设置tail节点时,是基于CAS的compareAndSetTail(Node expect, Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点正式与之前的尾节点建立关联。注意:可能有很多失败线程,用CAS操作保证了添加尾节点是线程安全的。
如上图所示,设置head节时,是通过获取同步状态成功的线程完成释放后,唤醒head节点的后继节点,后继节点将会在获取同步状态成功时将自己设置为首节点。注意:首节点是获取同步状态成功的节点,只唤醒一个后继节点线程无需CAS操作。
2. 独占式同步状态获取与释放
a. 获取同步状态
以下代码,acquire(int arg)来获取同步状态,其主要完成tryAcquire(arg)获取同步状态、addWaiter()完成Node构造、acquireQueued()完成Node添加到同步队列尾部及同步队列自旋方式获取同步状态。
/**
* 独占式忽略中断的获取同步状态
* 1. tryAcquire()成功后,其他线程进入同步队列中;
* 2. tryAcquire()失败后,
* a. 当前线程被阻塞进入等待状态;
* b. 当前线程及等待状态等线程信息构造成节点Node,参考addWaiter();
* c. Node添加到同步队列尾部进行自旋(for(;;)死循环);
*/
public final void acquire(int arg) {
// 尝试获取同步状态失败 + 当前线程添加到同步队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 当前线程及等待状态等线程信息构造成节点Node
*/
private AbstractQueuedSynchronizer.Node addWaiter(AbstractQueuedSynchronizer.Node mode) {
AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
AbstractQueuedSynchronizer.Node pred = tail;
// 当前tail的前节点不为null
if (pred != null) {
node.prev = pred;
// CAS操作设置tail,确保节点被线程安全添加
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当前tail的前节点为null
// 死循环(for(;;))确保节点正确添加
enq(node);
return node;
}
/**
* Node添加到同步队列尾部进行自旋
*/
final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 同步队列中每个节点自旋获取同步状态
for (;;) {
final AbstractQueuedSynchronizer.Node p = node.predecessor();
/*
* a. 当前节点的前节点是head;
* b. head能够获取获取同步状态成功;
* c. 当前节点设置为设置为head
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如上图所示,同步队列中的节点通过自旋方式获取同步状态,节点之间互不通信,只是简单判断自己的前节点是不是head节点即可。这是为什么?原因如下:
- head是成功获取同步状态的节点,释放同步状态后,只会唤醒其后继节点,而后节点唤醒后检测自己的前节点是不是head
- 维护同步队列的FIFO原则
b. 释放同步状态
如下代码所示,首节点调用release(int arg)释放同步状态之后,unparkSuccessor(Node node)方法使用LockSupport会唤醒处于等待状态的后继节点。
/**
* 释放同步状态
* 注意:释放同步状态的节点是head节点
* a. tryRelease(arg)返回true,表示释放成功;
* b. 通过LockSupport.unpark()唤醒后继节点
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
AbstractQueuedSynchronizer.Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3. 共享式同步状态获取与释放
共享式获取有多个线程同时获取到同步状态,这与独占式获取主要区别。如,写操作要求对资源的独占式访问,而读操作则为共享式访问。
a. 获取同步状态
以下代码,acquireShared(int arg)来获取同步状态,注意:
- tryAcquireShared(arg) >= 0时,表示获取同步状态
- tryAcquireShared(arg) < 0时,表示获取失败,进入自旋不断获取同步状态,成功后退出自旋
/**
* 共享式忽略中断的获取同步状态
* a. tryAcquireShared(arg) >= 0时,表示获取同步状态
* b. tryAcquireShared(arg) < 0时,表示获取失败
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 自旋方式获取同步状态,tryAcquireShared(arg) >= 0时,退出自旋
*/
private void doAcquireShared(int arg) {
final AbstractQueuedSynchronizer.Node node = addWaiter(AbstractQueuedSynchronizer.Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final AbstractQueuedSynchronizer.Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
// 成功获取同步状态,退出自旋
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
b. 释放同步状态
与独占式的主要区别:由于多线程共享获取同步状态,tryReleaseShared()释放同步状态时通过循环和CAS保证线程安全释放同步状态。
/**
* 释放同步状态
* 注意:tryReleaseShared(arg),由于共享多个线程获取同步状态,
* 释放同步状态时通过循环和CAS保证线程安全释放同步状态
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
4. 独占式超时获取同步状态
如下代码所示,tryAcquireNanos(int arg, long nanosTimeout)超时时,返回false,即:获取同步状态失败。它是在响应中断acquireInterruptibly(int arg)方法上增加了超时限制,而响应中断则是在acquire(int arg)增加了中断响应。如下图所示是,独占式超时获取同步状态的流程。
/**
* 独占式超时获取同步状态
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
/**
* 在nanosTimeout时间内还没有获取同步状态,则返回false,获取失败
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final AbstractQueuedSynchronizer.Node node = addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final AbstractQueuedSynchronizer.Node p = node.predecessor();
// 与独占式获取相同
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 超时时间计算
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 该线程继续等待状态nanosTimeout
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
5. 同步器的API
如下表所示,同步器的API主要分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况等API。
三、Condition接口
Condition接口的作用:配合Lock接口进行线程间之间的通信(等待/通知),类似于Object类中的wait()和notify()的等待/通知机制。Condition对象需要Lock,即:lock.newCondition()。
Condition接口实现类是ConditionObject,它是AbstractQueuedLongSynchronizer内部类,主要包括:等待队列、等待、通知。
1. Object与Condition的等待/通知对比
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock拥有一个同步队列和多个等待队列。
2. 等待队列
如下图所示,等待队列是FIFO单向队列。当前线程调用condition.await()后,当前线程改为等待状态并释放同步状态state,随后该线程构造成新的Node节点加入到等待队列尾部;当其他线程调用condition.signal()后唤醒等待的线程,并添加到同步队列。
Condition结构包含了:一个等待队列、首节点(firstWaiter)、尾节点(lastWaiter)。但是整个Lock有多个等待队列,原因是:锁可能具有重入性、共享性。
设置lastWaiter时,并没有使用CAS保证,原因是调用await()方式,实际上该线程已经获取了锁,由锁来保证线程安全。
3. 等待await()
如下图所示,调用condition.await()后,当前线程(head节点)释放锁,同时进入等待状态、构造新Node加入到等待队列中。从队列看:
- 同步队列:head节点释放锁,进入等待状态,移除head;唤醒后继节点
- 等待队列:构造新Node加入到等待队列并设置lastWaiter
4. 通知signal()
如下图所示,调用condition.signal()后,将会唤醒在等待队列中等待时间最长的节点(首节点),首节点移动到同步队列的尾部。当成功获取同步状态之后,被唤醒的线程将从先前调用的await()方法返回,继续执行后续代码。
四、参考资料
深入理解AQS(AbstractQueuedSynchronizer)_晨初听雨的博客-CSDN博客_深入理解aqs
Java锁--Lock实现原理(底层实现)_静_默的博客-CSDN博客_lock底层原理
详解synchronized与Lock的区别与使用_淳安郭富城的博客-CSDN博客_synchronized和lock的区别