1.Lock接口
- 使用时需要显式地获取和释放锁
- 缺少了隐式获取释放锁的便捷性,但拥有了锁获取与释放的可操作性、可中断的获取锁以
及超时获取锁等多种synchronized关键字所不具备的同步特性 - Lock接口的实现基本都是通过聚合一个同步器的子类来完成线程访问控制的(可以参考队列同步器中的自定义同步组件——TwinsLock部分)
不要将获取锁的过程写在try块中(如果在获取自定义锁时发生了异常,异常抛出的同时会导致锁无故释放)
2.队列同步器
-
用来构建锁或者其他同步组件的基础框架
-
使用int成员变量表示同步状态(如ReentrantLock锁的对应自定义同步器的实现中,同步状态表示锁被一个线程重复获取的次数),通过内置的FIFO队列来完成资源获取线程的排队工作
-
既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态
-
锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作
-
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态
-
同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况
2.1 队列同步器的接口与示例
class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
... 同步器其他方法的实现
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
// 下面方法是Mutex的方法,但实际调用的是静态内部类Sync的方法
public boolean isLocked() { return sync.isHeldExclusively(); }
...其他调用了Sync方法的方法
}
2.2 队列同步器的实现分析
2.2.1 .同步队列
-
同步队列中的节点用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点
-
同步器拥有首节点和尾节点,没有成功获取同步状态的线程将会成为节点加入该队列的尾部
-
同步器提供了一个基于CAS的设置尾节点的方法:
compareAndSetTail(Node expect,Node update)
,它需要传递当前线程“认为”的尾节点和当前节点 -
设置首节点是通过获取同步状态成功的线程来完成,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证
2.2.2 独占式同步状态获取与释放
同步状态获取:
public final void acquire(int arg) {
// tryAcquire:保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点
// addWaiter:将Node.EXCLUSIVE节点(该节点保证同一时刻只能有一个线程成功获取同步状态)加入到同步队列的尾部
// acquireQueued:使得该节点以“死循环”的方式获取同步状态(未成功获取的节点中的线程被阻塞,节点依旧在自旋。阻塞线程需要通过前驱节点(需要是头节点)的出队或被中断来唤醒)
// 注意:节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态:
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(int arg)
方法释放同步状态,然后唤醒头节点的后继节点
2.2.3 共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态
-
在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是
tryAcquireShared(int arg)
方法返回值大于等于0 -
在释放同步状态时,和独占式主要区别在于
tryReleaseShared(int arg)
方法必须确保同步状态线程安全释放(一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程)
2.2.4 独占式超时获取同步状态
- 调用同步器的
doAcquireNanos(int arg,long nanosTimeout)
方法可以超时获取同步状
态(即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false) - 独占式超时获取同步状态和独占式获取同步状态主要区别在于未获取到同步状态时的处理逻辑:
acquire(int args)
在未获取到同步状态时,将会使当前线程一直处于等待状态doAcquireNanos(int arg,long nanosTimeout)
会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回
3.重入锁
-
重入锁ReentrantLock是支持重进入的锁,该锁支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择
-
synchronized关键字隐式的支持重进入
-
如果在绝对时间上,先对锁进行获取的请求一定先被满足,则该锁是公平的(FIFO),反之是不公平的
3.1 实现重进入
实现重进入需要解决以下两个问题:
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
- 锁的最终释放:锁对于获取进行计数自增(计数表示当前锁被重复获取的次数);锁被释放时,计数自减,当计数等于0时表示锁已经成功释放
3.2 公平与非公平获取锁的区别
- 公平性与否是针对获取锁而言的,如果一个锁是公平的,则锁的获取顺序就应该符合请求的绝对时间顺序(即FIFO)
ReentrantLock
中获取两种锁的实现不同:
// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// *只要CAS设置同步状态成功,则表示当前线程获取了锁
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); // 将同步状态值进行增加并返回true
return true;
}
return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// *相较于非公平锁多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁
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;
}
- 公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换;非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量
4.读写锁
-
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞
-
要实现对一个共享缓存的写操作后的更新对后续的读操作可见:
- 没有读写锁:写操作开始则所有晚于写操作的读操作进入等待状态。写操作完成进行通知,等待的读操作开始执行(等待通知机制)
- 使用读写锁:在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行
-
读写锁的性能都会比排它锁好(大多数场景读是多于写的),它能够提供比排它锁更好的并发性和吞吐量
-
JUC中提供读写锁的实现是ReentrantReadWriteLock,该锁特性如下:
4.1 读写锁的实现分析
4.1.1 读写状态的设计
-
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态(ReentrantLock中自定义同步器的同步状态表示锁被一个线程重复获取的次数)
-
读写锁将表示同步状态的整型变量按位切分为两个部分:
4.1.2 写锁的获取与释放
- 写锁的获取:
- 如果当前线程已经获取了写锁,则增加写状态;如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态
- 存在读锁,则写锁不能被获取:如果允许读锁在已被获取的情况下对写锁的获取,则正在运行的其他读线程就无法感知到当前写线程的操作
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// c!=0表示32位整型变量不为0
if (c != 0) {
// w==0表示写状态为0,则读状态r一定不为0(因为c=w+r)
// 存在读锁或者当前获取线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
- 写锁的释放:每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁
4.1.3 读锁的获取与释放
- 读锁的获取:
- 如果当前线程已经获取了读锁,则增加读状态;如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态
- 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程增加读状态,成功获取读锁
protected final int tryAcquireShared(int unused) {
for (;;) {
int c = getState();
int nextc = c + (1 << 16); // 增加读状态(需要向左移动16位)
if (nextc < c)
throw new Error("Maximum lock count exceeded");
if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
return -1;
// CAS保证增加读状态时的线程安全
if (compareAndSetState(c, nextc))
return 1;
}
}
- 读锁的释放:读锁的每次释放均减少读状态,减少的值是(1<<16)
4.1.4 锁降级
- 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
注意:当前线程有写锁,然后释放,再获取读锁,这不能成为锁降级
- RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程),目的也是保证数据可见性(如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的)