并发编程的艺术-学习
第五章 Java中的锁
5.1 Lock接口
Lock接口显示操作锁,synchronized隐式锁。
Lock接口特性
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果该时刻没有被其他线程获取到,则成功获取并持有锁 |
能被中断的获取锁 | 与Synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
5.2 队列同步器
AbstractQueuedSynchronizer队列同步器,是用来构建锁或者其他同步组件的基础框架,使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
队列同步器的使用方式是继承。子类通过继承同步器并实现它的抽象方法来管理同步状态。更改状态的方法getState()、setState(int newState)和compareAndSetState(int expect, int update)。
队列同步器是接口,方便实现不同类型的同步组件(如ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
锁和同步器很好地隔离了使用者和实现者关注的领域。
5.2.1 队列同步器的接口与示例
同步器的设计:基于模板方法模式
使用者需要继承同步器并重写指定的方法。
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。
5.2.2 队列同步器的实现分析
- 同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下:
属性类型与名称 | 描述 |
---|---|
Int waitStatus | 等待状态。1 CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化;2、 SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被曲线,将会通知后继节点,使后继节点的线程得以运行;3、 CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到对同步状态的获取中;4、PROGAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去;5、INITIAL,值为0,初始状态 |
Node prev | 前驱节点,当节点加入同步队列时被设置(尾部添加) |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是SHARED常量,即节点类型(独占和共享)和等待队列中的后继节点共用一个字段 |
Thread thread | 获取同步状态的线程 |
同步器提供了基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node updte),需要传递当前线程“认为”的尾节点和当前节点,只有设置成功,当前节点才正式与之前的尾节点建立关联。
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,只需要将首节点设置成原首节点的后继节点并断开原首节点的next引用即可。
2、独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,主要逻辑:首先tryAcquire()保证线程安全的获取同步状态,如果获取失败,则构造同步节点并通过addWaiter()将该节点加入到同步队列的尾部,最后调用acquireQueued()使该节点以“死循环”的方式获取同步状态。如果获取不到阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
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;
}
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
使用compareAndSetTail(Node expect, Node update)确保节点能被线程安全添加。
同步器通过“死循环”来保证节点的正确添加,在死循环中只有通过CAS将节点设置成为尾节点后,线程才能返回。enq()将并发添加节点的请求通过CAS变得“串行化”。
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);
}
}
}
只有前驱节点是头节点才能尝试获取同步状态,因为:
第一, 头节点是成功获取同步状态的节点,而节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。(不懂,后继节点被唤醒后,为啥还要检查自己的前驱节点是否是头节点?)
第二, 维护同步队列的FIFO原则。
独占式同步状态获取流程:
通过调用同步器的release()方法可以释放同步状态,释放后,或唤醒其后继节点。
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()释放同步状态,然后唤醒头节点的后继节点。
3、 共享式同步状态获取与释放
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean intrrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;
if (intrrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
intrrupted = true;
}
}
} finally {
if (failed) {
cancalAcquire(node);
}
}
}
4、 独占式超时获取同步状态
5、 自定义同步组件——TwinsLock
5.3 重入锁
重入锁ReentrantLock,表示支持一个线程对资源的重复加锁。支持公平、非公平性选择。
占有锁的线程再次获取锁的场景,被自己阻塞(不懂);
Synchronized隐式重进入(为什么可以重进入);
1、 实现重进入
重进入是指:任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
问题:
1) 线程再次获取锁。锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2) 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性为例。
2、 公平与非公平获取锁的区别
公平:hasQueuedPredecessors加入了同步队列中当前节点是否有前驱节点的判断,如果返回true,表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。保证了FIFO原则,切换量大。
非公平锁可能使线程“饥饿”,但切换少,保证了更大吞吐量。
5.4 读写锁
读写锁:可以同一时刻运行多个读线程访问,但写线程访问时,所有的读线程和写线程均被阻塞。
读写锁之前->等待通知机制,当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步)
读写锁的性能比排他锁好,因为读多于写。读写锁能提供更好的并发性和吞吐量。
ReentrantReadWriteLock
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能再次获取读锁。而写线程在获取写锁之后能再次获取写锁,同时也可以获取读锁 |
锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级成为读锁(不懂) |
5.4.1 读写锁的接口与示例
ReadWriteLock仅定义了获取读锁和写锁的方法readLock()、writeLock(),其实现ReentrantReadWriteLock除了接口方法之外,还提供了便于外界监控其内部工作状态的方法。
方法名称 | 描述 |
---|---|
int getReadLockCount() | 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数。例如,仅一个线程,连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,当方法返回n |
int getReadHoldCount() | 返回当前线程获取读锁的次数。使用ThreadLocal保存当前线程获取的次数 |
boolean is WriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
5.4.2 读写锁的实现分析
1、读写状态的设计
读写锁依赖自定义同步器来实现同步功能。
整型变量,按位切割使用,高16位表示读,低16位表示写。位运算。
假设当前同步状态为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
S不等于0时,当写状态等于0,则读状态大于0,即读锁已被获取。
2、写锁的获取与释放
写锁是一个支持重进入的排它锁。
获取:
当前线程获取写锁,则增加写状态;当前线程获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
条件:重入(当前线程为获取了写锁的线程)、读锁是否存在
原因:读写锁要确保写锁的操作对读锁可见。
释放:
减少写状态,直到等于0,表示写锁已被释放。
3、读锁的获取与释放
读锁是一个支持重进入的共享锁。
每个线程各自获取读锁的次说存在ThreadLocal线程变量中(线程安全)
如果其他线程已经获取写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程增加读状态,成功获取读锁。(不懂:写锁被获取,怎么还能获取读锁)
4、锁降级
指当前线程把持写锁,再获取读锁,随后释放写锁的过程。
锁降级中读锁的获取是必要的。保证数据的可见性。
ReentrantReadWriteLock不支持锁升级,保证数据可见性。因为读锁获取后,当升级为写锁更新了数据,但是对其他获取到读锁的线程不可见。
5.5 LockSupport工具
阻塞或唤醒线程时,使用LockSupport工具类来完成相应工作。
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回 |
void parkNanos(long nanos) | 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回 |
void parkUntil(long deadline) | 阻塞当前线程,知道deadline时间 |
void unpark(Thread thread) | 唤醒处于阻塞状态的线程thread |
方法park(Object blocker)、parkNanos(Object blocker,long nanos)、parkUntil(Object blocker,long deadline)可以提供当前线程等待的对象(阻塞对象)
5.6 Condition接口
Object的监视器方法与Condition接口的对比
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock()获取锁;调用Lock.newCondition()获取Condition对象 |
调用方式 | 直接调用,如object.wait() | 直接调用,如condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应中断(不懂?) | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来摸个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持支持 | |
唤醒等待队列中的全部线程 | 支持 | 支持 |
5.6.1 Condition接口与示例
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
Condition的方法及描述
方法名称 | 描述 |
---|---|
void await() throws InterruptedException | 当前线程进入等待状态直到被通知(signal)或中断,当前线程进入运行状态且从await()方法返回的情况,包括:其他线程调用Condition的signal()或signalAll(),而当前线程被选中唤醒。a.其他线程(调用interrupt())中断当前线程;b.如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁(不懂) |
void awaitUninterruptibly() | 当前线程进入等待状态直到被通知,从方法名称上看该方法对中断不敏感 |
long awaitNanos(long nanosTimeout) throws InterruptedException | 当前线程进入等待状态直到被通知、中断或超时。返回值表示剩余的时间。如果没有到指定时间就被通知,方法返回true,否则表示到了指定时间,方法返回false |
boolean awaitUntil(Date deadline) throws InterruptedException | 当前线程进入等待状态直到被通知、中断或某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false |
void signal() | 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁 |
void signalAll() | 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁 |
有界队列是一种特殊队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现空位。
5.6.2 Condition的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类较为合理。每个Condition对象都包含一个队列(等待队列),该队列是实现等待/通知的关键。
1、等待队列
等待队列是一个FIFO的队列,队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.wait()方法,那么该线程将会释放锁、构造节点假如等待队列并进入等待状态。
节点引用更新过程并没有使用CAS保证,因为调用await()方法的线程必定是获取了锁的线程,所以该过程由锁来保证线程安全。
Object监视器模型:一个对象拥有一个同步队列、一个等待队列
Lock:一个同步队列、多个等待队列(不懂)
2、等待
3、通知
首先获取锁,接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
被唤醒后的线程,将从await()的while循环中退出,进而调用同步器的acquireQueued()假如到获取同步状态的竞争中。
成功获取同步状态之后,被唤醒的线程将先从之前调用的await()返回,此时该线程已经成功获取了锁。
5.7 本章小结
本章介绍了java并发包中与锁相关的API和组件:队列同步器、重入锁、读写锁以及Condition