Java语言本身就已经提供了Sychronized的语法糖,使用起来比较简单,并且在jdk1.5以后已经得到了优化,性能也是不差的,为什么java还需要提供较为复杂的java sdk来重复造轮子?他们的区别在哪?J.U.C是java.util.concurrent包的简称,在JDK1.5添加,目的是为了开发者能够灵活的运用多线程编程。本文主要讲解Lock和Condition,Lock和Condition就是java.util.concurrent包下的接口
Lock概述
类图:
我们先来回答为什么有了Synchroniezed还需要Lock呢?根据java线程-synchronized详解中了解到synchronized的缺点
- synchronized不能设置公平锁
- synchronized不能响应中断
- synchronized不支持超时
- synchronized不支持非阻塞获取锁
- synchronized不支持读写锁精细化管理
- synchronized不支持乐观锁
但这些缺点Lock锁都可以解决,其中Lock解决了1-4的缺点,ReadWriteLock解决了缺点5,StampedLock解决了缺点6。当然ReadWriteLock也包含解决了缺点1-4,StampedLock包含解决了缺点1-5。
Lock结构:
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException; // 支持中断的 API
boolean tryLock(); // 支持非阻塞获取锁的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持超时的APi
void unlock();
Condition newCondition(); // 条件变量
}
ReentrantLock:
public class ReentrantLock implements Lock, java.io.Serializable{
public ReentrantLock() { // 默认是非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) { // 使用构造器来判断是否使用公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
}
使用Lock锁的最佳实践:
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
ReadWriteLock结构:
public interface ReadWriteLock {
Lock readLock(); // 读锁
Lock writeLock(); // 写锁
}
StampedLock简介:
StampedLock是比ReentrantReadWriteLock更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock不同的是,StampedLock支持多个线程申请乐观读的同时,还允许一个线程申请写锁。
乐观读并不加锁
StampedLock的底层并不是基于AQS的。
ReentrantLock概述
使用Lock锁的示例:
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
Lock保证可见性的基本原理是利用volatile以及Happens-Before的规则,其中ReentrantLock内部维护了一个内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。利用的规则如下:
- 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
- volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
- 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
具体实现原理还是通过AQS,AQS的具体原理后面会有讲解。
可重入锁
ReentrantLock翻译过来就是可重入锁,可重入锁指的是可以被同一个线程多次加锁的锁。注意,这里说的多次加锁,并不是说解锁之后再次加锁,而是在锁没有解锁前再次加锁。
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
increment(); // 重入锁
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
public void increment(){
rtl.lock();
try{
value+=1;
}catch(){
rtl.unlock();
}
}
JUC提供的锁都是可重入锁。实际上,Java synchronized内置锁也是可重入锁。从侧面上,我们也可以得出,可重入是对锁的基本要求。为了实现可重入特性,可重入锁中需要有一个变量来记录重入的次数。每重入一次,变量就增一;每解锁一次(调用unlock()或退出synchronized代码块),变量就减一,直到变量值为0时,才会释放锁唤醒其他线程执行。
公平锁
ReentrantLock有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。
前面我们在介绍Synchronized实现原理的时候,多个线程竞争Monitor锁的时候,如果一个线程没有获得锁,就会进入等待队列_cxq
,并且还需要调用park()函数阻塞自己。当有线程释放锁的时候,会从_EntryList
中唤醒一个等待的线程,取消阻塞状态,让这个线程和其他线程一起竞争Monitor锁,而不是直接将锁给它。
想要实现公平锁,新的线程想要获取锁,如果_EntryList
和_cxq
队列中只要有排队等待的线程,那么不管有没有线程释放,只要是新来的线程,直接将线程放入到_cxq
队列中,按照FIFO的顺序来等待锁,以免出现插队的现象。
只不过非公平锁的实现是在jvm层实现的,但公平锁的实现是在jdk层通过AQS实现的。
如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。
可中断锁
对于synchronized锁来说,线程在阻塞等待synchronized锁时是无法响应中断的。而JUC Lock接口提供了lockInterruptibly()函数,支持可响应中断的方式来请求锁。示例代码如下所示。
public class LockTest {
private Lock lock = new ReentrantLock();
public void doBussiness() {
String name = Thread.currentThread().getName();
try {
lock.lockInterruptibly();
for (int i=0; i<5; i++) {
Thread.sleep(1000);
System.out.println(name + " : " + i);
}
} catch (InterruptedException e) {
System.out.println(name + " 做些别的事情");
} finally {
try {
lock.unlock();
System.out.println(name + " 释放锁");
} catch (Exception e) {
System.out.println(name + " : 没有得到锁的线程运行结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
Thread t0 = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
Thread t1 = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
// 启动线程t1
t0.start();
Thread.sleep(10);
// 启动线程t2
t1.start();
Thread.sleep(100);
// 线程t1没有得到锁,中断t1的等待
t1.interrupt();
}
}
可中断锁一般用于线程管理中,方便关闭正在执行的线程。比如,Nginx服务器采用多线程来执行请求。当我们调用stop命令关闭Nginx服务器时,Nginx服务器可以采用中断的方式,将阻塞等待锁的线程中止,然后,合理的释放资源和妥善处理未执行完成的请求,以实现服务器的优雅关闭。
非阻塞锁
锁已经被另一个线程获取,那么,这个线程就需要阻塞等待。JUC Lock接口提供了tryLock()函数,支持非阻塞的方式获取锁。如果锁已经被其他线程获取,那么,调用tryLock()函数会直接返回错误码而非阻塞等待。
可超时锁
还提供给了带时间参数的tryLock()函数,支持非阻塞获取锁的同时设置超时时间。也就是说,一个线程在请求锁时,如果这个锁被其他线程持有,那么这个线程会阻塞等待一段时间。如果超过了设定的超时时间,线程仍然没有获取到锁,那么tryLock()函数将会返回错误码而不再阻塞等待。
ReentrantLock与Synchronized的区别
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖AQS(JDK层) | 依赖ObjectMonitor(JVM层),Monitor锁 |
灵活性 | 支持响应中断,非阻塞获取锁,超时 | 不支持响应中断,不支持非阻塞获取锁,不支持超时 |
释放方式 | 显式调用unlock() | 隐式自动释放Monitor锁 |
锁类型 | 支持公平与非公平锁 | 仅支持非公平锁 |
可重入性 | 可重入 | 可重入 |
条件队列 | 可关联多个条件队列 | 仅能关联一个条件队列 |
ReadWriteLock概述
ReentrantReadWriteLock是ReadWriteLock的实现类,ReentrantLock具有的特性,它也具有。
实际上,在多线程环境下,读操作是可以并发执行的,但是读和写,写和写不能并发执行。为了提高读操作的效率,将读写锁分离,JUC提供ReadWriteLock
接口和ReentrantReadWriteLock
实现类。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
其中读锁是共享锁,多个读操作可以并发执行。写锁是排它锁,写锁同时只能由一个线程持有。读锁与写锁之间也是排它的。因此读写锁的使用场景就是读多写少的场景。
ReadWriteLock基本使用
代码使用示例:
public class ReentrantReadWriteLockDemo {
private final List<String> strList = new LinkedList<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void add(int idx, String str) {
writeLock.lock();
try {
strList.add(idx, str);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}
public String get(int idx) {
readLock.lock();
try {
return strList.get(idx);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
readLock.unlock();
}
}
}
ReentrantReadWriteLock同样也支持公平锁和非公平锁的指定,也是通过构造器参数来区分的,默认是非公平锁。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
读写锁的升降级
如何使用ReentrantReadWriteLock实现一个Cache?代码如下:
public class Cache<K, V> {
private final Map<K, V> m = new HashMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
// 错误示例
public final V get(K key) {
V v = null;
// 读缓存
r.lock();
try {
v = m.get(key);
if (v == null) { // 如果v为null
w.lock(); // 写锁加锁
try {
// v = 省略代码
m.put(key, v);
} finally {
w.unlock(); // 写锁解锁
}
}
} finally {
r.unlock();
}
return v;
}
}
上述代码中可以发现,先获取读锁,如果v为null,就会再次获取写锁,这样看上去好像是没有问题的,这个是锁的升级。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的,这个你一定要注意。这是因为一个线程获取到读锁,可能其他线程同时也获取到读锁,如果这个线程升级到了写锁,那么其他线程的读锁和写锁就不互斥了。
虽说锁的升级不允许,但是锁的降级是允许的。锁的降级就是从写锁降级到读锁。
参考官方示例代码:
public class CacheData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
void processCacheData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
// 处理数据
cacheValid = true;
}
// 释放写锁前,降级为读锁
r.lock();
} finally {
w.unlock();
}
}
// 此处任有读锁
try {
// 使用数据
} finally {
r.unlock();
}
}
}
StampedLock概述
StampedLock是对ReadWriteLock的进一步优化,在读锁和写锁的基础之上,又提供了乐观读锁。实际上,乐观读锁并没有加任何锁。在读多写少的应用场景中,大部分读操作都不会被写操作干扰,因此,我们甚至可以将读锁也省略掉。只有验证读操作真正有被写操作干扰的情况下,线程再加读锁重复执行读操作。
StampedLock基本使用
ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。相关的示例代码如下。
public class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private final List<String> list = new LinkedList<>();
public void add(int idx, String str) {
long stamped = stampedLock.writeLock();
try {
list.add(idx, str);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
stampedLock.unlockWrite(stamped);
}
}
public String get(int idx) {
long stamped = stampedLock.tryOptimisticRead(); // 乐观读
String res = list.get(idx);
if (stampedLock.validate(stamped)) { // 如果没有写操作干扰
return res;
}
// 有写操作干扰,重新获取读锁
stamped = stampedLock.readLock();
try {
return list.get(idx);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
stampedLock.unlockRead(stamped);
}
}
}
StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。
StampedLock使用注意事项
- StampedLock 不支持重入。
- StampedLock 的悲观读锁、写锁都不支持条件变量。
- 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。如果需要中断,建议使用readLockInterruptibly() 和写锁 writeLockInterruptibly()。
AQS具体实现原理
AQS简介
AQS是抽象类AbstractQueuedSynchronizer
的简称,中文翻译为抽象队列同步器。Synchronized主要依赖JVM中的ObjectMonitor类来实现,但JUC中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AQS实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
AQS成员变量
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
private transient Thread exclusiveOwnerThread; // 父类AbstractOwnableSynchronizer中的成员变量
-
state
我们在使用synchronized,当多个线程竞争锁的时候,jvm会让他们通过CAS操作来设置ObjectMonitor中的
_owner
字段,谁设置成功,谁就获取了这个锁。AQS中的state的作用就类似于
_owner
字段,但不同的是_owner
存储的是获取锁的线程,而state是一个int类型的变量,存储的是0,1等整数值,0表示锁没有被占用,1表示已经被占用,大于1的数表示重入的次数。当多个线程竞争锁的时候,它们会通过Unsafe类提供的CAS函数(保证了原子性)来更新state值。这里CAS指的是先检查state的值是否为0,如果是的话,将state设置为1.谁设置成功,谁就获取到锁。不过独占锁和共享锁state的设置逻辑会有些不一样
独占模式:
共享模式:
-
exclusiveOwnerThread
存储持有锁的线程,它配合state成员变量,可以实现锁的重入机制。
-
head和tail
在ObjectMonitor中,
_cxq
、_EntryList
用来存储等待锁的线程,_WaitSet
用来存储调用了wait()函数的线程。在AQS中只有一个等待队列,既用来存储等待锁的线程,也用来存储等待条件变量的线程。这个等待队列是个双向链表,它还有个名称,叫做CLH队列。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
AQS中的队列是通过双向链表实现的,头节点为虚拟节点,其实并不存储任何信息,只是占位。真正的第一个数据节点,是在第二个节点开始的。
AQS数据结构
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 prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
解释一下几个方法和属性值的含义:
方法和属性值 | 含义 |
---|---|
waitStatus | 当前节点在队列中的状态 |
thread | 表示处于该节点的线程 |
prev | 前驱指针 |
predecessor | 返回前驱节点,没有的话抛出npe |
nextWaiter | 指向下一个处于CONDITION状态的节点 |
next | 后继指针 |
线程两种锁的模式:
模式 | 含义 |
---|---|
SHARED | 表示线程以共享的模式等待锁 |
EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus有下面几个枚举值:
枚举 | 含义 |
---|---|
0 | 当一个Node被初始化的时候的默认值 |
CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 |
SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
AQS成员方法
AQS是通过模板方法实现的,AQS定义了8个模版方法,4个抽象方法,这几个方法可以分为2组,分别用于AQS的两种工作模式:独占模式和共享模式,其中不带Shared后缀的为独占模式,带Shared后缀的为共享模式。
Lock为排它锁,因此Lock的底层实现只会用到AQS的独占模式。ReadWriteLock中的读锁为共享锁,写锁为排它锁。Semaphore、CountDownLatch这些同步工具只会用到AQS的共享模式。
8个模版方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
四个抽象方法:
这四个方法都提供了默认实现,抛出UnsupportedOperationException异常。这样做是为了减少开发量,即我们不需要在子类中实现所有的抽象方法。
其中四个方法的参数含义为:获取锁的次数,尝试获取资源
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
还有一个方法:该线程是否正在独占资源。只有用到Condition才需要去实现它。
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
AQS具体实现类
AQS具体实现类有:
- ReentrantLock里的NonfairSync和FairSync,这两个类又有共同的父类Sync。
- ReentrantReadWriteLock的NonfairSync和FairSync,这两个类又有共同的父类Sync。
- Semaphore里的NonfairSync和FairSync,这两个类又有共同的父类Sync。
- CountDownLatch里的Sync
- ThreadPoolExecutor里的Worker
ReentrantLock实现原理
先来分析一下ReentrantLock里的实现,看一下具体实现流程是如何的。
加锁流程
非公平锁加锁流程:
公平锁加锁流程:
-
通过ReentrantLock的加锁方法Lock进行加锁操作,实际上是调用Sync#lock(),由于Sync是抽象类,具体实现类还得看ReentrantLock初始化的时候选择的是公平锁和非公平锁。
-
非公平锁,会先进行CAS更新state,如果更新成功,表示当前线程已经被占用,设置独占线程就是当前线程。如果更新失败,再调用AQS.acquire(1)。
公平锁,则会直接调用AQS.acquire(1)。
AQS.acquire方法如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
AQS.acquire(1)会执行tryAcquire()方法,tryAcquire也是需要子类来实现。
非公平锁的实现逻辑是调用其父类Sync#nonfairTryAcquire()方法
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//1. 锁没有被其他线程占用 if (compareAndSetState(0, acquires)) { // CAS设置state=1 setExclusiveOwnerThread(current); // 设置独占线程为当前线程 return true; // 获取锁成功 } } else if (current == getExclusiveOwnerThread()) { // 2. 锁可重入 int nextc = c + acquires; if (nextc < 0) // overflow, 重入次数太多,超过了int的最大值会溢出,为负数 throw new Error("Maximum lock count exceeded"); setState(nextc); // state记录重入的次数,解锁的时候会用到 return true; // 获取锁成功 } return false; // 3. 锁被其他线程占用 }
公平锁的实现逻辑是如下:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//1. 锁没有被其他线程占用 if (!hasQueuedPredecessors() && // 等待队列中没有线程时才能获取锁 compareAndSetState(0, acquires)) { // CAS设置state=1 setExclusiveOwnerThread(current);// 设置独占线程为当前线程 return true;// 获取锁成功 } } else if (current == getExclusiveOwnerThread()) {// 2. 锁可重入 int nextc = c + acquires; if (nextc < 0)// 重入次数太多,超过了int的最大值会溢出,为负数 throw new Error("Maximum lock count exceeded"); setState(nextc); // state记录重入的次数,解锁的时候会用到 return true; // 获取锁成功 } return false; // 3. 锁被其他线程占用 }
-
如果获取锁失败,会将线程加入到等待队列里,会调用addWaiter()方法
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) { // 自旋执行CAS操作,直到成功为止 for (;;) { Node t = tail; if (t == null) { // 链表为空,添加虚拟头节点 // CAS操作解决添加虚拟头节点的线程安全问题 if (compareAndSetHead(new Node())) tail = head; } else { // 链表不为空 node.prev = t; // CAS操作解决了同时往链表尾部添加节点时的线程安全问题 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
-
最后acquireQueued,主要包含两个逻辑:使用tryAcquire()函数来竞争锁和使用park()函数来阻塞线程,并使用for循环来交替执行这两个逻辑。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 自旋(竞争锁+阻塞),因为被唤醒之后不一定能竞争到锁,所以要自旋 for (;;) { final Node p = node.predecessor(); // 如果线程是被中断唤醒的,那么p 就不一定等于head,也就不能去竞争锁 if (p == head && tryAcquire(arg)) { setHead(node); // 把node设置为虚拟头节点,也就相当于将它删除 p.next = null; // help GC failed = false; return interrupted; } // 调用park()函数来阻塞线程,线程被唤醒有以下两种情况: // 1. 其他线程调用unpark()来唤醒,此时,节点位于虚拟头节点的下一个,p == head // 2. 被中断唤醒,此时,节点不一定就是虚拟节点的下一下,p不一定等于head if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 以上过程只要出现异常,都要将这个节点标记为cancelled,等待被删除 if (failed) cancelAcquire(node); } } private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
解锁流程
- 通过ReentrantLock的unlock方法解锁
- unlock会调用AQS#release()方法
- AQS#release()会调用tryRelease()方法,这个方法是ReentrantLock.Sync的公共的方法,并不区分是公平锁和非公平锁
- 释放锁的时候会调用JVM#unpark()函数。
具体代码如下:
// AQS.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // JVM#unpark()
return true;
}
return false;
}
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是独占线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // state - 1 == 0解锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // != 0表示锁被多次重入,还不能解锁
return free;
}
中断机制和超时机制
之前说过Lock支持线程中断和超时机制,对应的方法是分别是
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
中断机制
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 如果线程中断
throw new InterruptedException(); // 则抛出异常
if (!tryAcquire(arg)) // 如果获取锁成功直接返回,获取锁失败就调用doAcquireInterruptibly
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) // 与acquireQueued()函数实现类似
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 与acquireQueued()不同的地方是这里:这里直接抛出异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
超时机制
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted()) // 如果线程中断
throw new InterruptedException(); // 抛出异常
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout); // 如果成功获取锁,则直接返回,否则调用doAcquireNanos函数
}
// 在acquireInterruptibly基础上添加了对超时的处理机制
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final 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) // 不着急阻塞,先自旋一下
LockSupport.parkNanos(this, nanosTimeout); // 超时阻塞
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
ReentrantReadWriteLock实现原理
ReentrantReadWriteLock是ReadWriteLock的实现类,跟ReentrantLock的结构类似,内部也有Sync类继承自AQS,以及Sync的两个子类,NonfairSync和FairSync来实现读锁(ReadLock)和写锁(WriteLock)。ReadLock和WriteLock均实现了Lock接口,实现了Lock接口中的所有加锁和解锁函数,且使用相同的AQS。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁、写锁可重入最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//获取低16位的条件
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
protected final boolean tryRelease(int releases) {}
protected final boolean tryAcquire(int acquires) {}
protected final boolean tryReleaseShared(int unused) {}
protected final int tryAcquireShared(int unused) {}
final boolean tryWriteLock() {}
final boolean tryReadLock() {}
}
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
}
}
通过对ReentrantLock实现原理的分析的值,AQS中的state值是用来表示加锁的情况的,0表示没有加锁,1表示已经加锁,大于1的值表示重入的次数。但是读写锁还是需要区分是读锁还是写锁,因为处理逻辑是不一样的。
state变量中的低16位表示写锁的使用情况,高16位表示读锁的使用情况。
低16位所表示的数,值为0表示没有加写锁,值为1表示已经加写锁,值大于1表示写锁的重入次数。
高16位所表示的数,值为0表示没有加读锁,值为1表示已经加读锁,值大于1表示读锁总共被获取了多少次。
写锁原理
我们先来看WriteLock#lock()函数的实现,实际上还是调用的是AQS#acquire(1),与ReentrantLock的区别还在于tryAcquire()函数,写锁的实现,不管是公平锁还是非公平锁,都在父类的Sync#tryAcquire()中实现。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // c为state的值
int w = exclusiveCount(c); // 低16位的值,也就是写锁的加锁情况
// 1. 已经加读锁或者写锁(state!=0)
if (c != 0) {
// 已加读锁(w==0)或者当前加写锁的线程不是自己
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;
}
// 2. 没有加锁(state == 0)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) // 去排队
return false;
setExclusiveOwnerThread(current);
return true; // 获取到了锁
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
unlock()的实现逻辑也跟ReentrantLock.unlock类似,但是tryRelease()会有区别,代码如下:
protected final boolean tryRelease(int releases) {
// tryRelease()是AQS工作在独占模式下的函数,只能用于排它锁,也就是写锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 更新state值,写入的重入次数-release,对于锁来说,release总是等于1
int nextc = getState() - releases;
// 只有更新之后的state值为0时,才可以将写锁释放。
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁原理
再来看看ReadLock#lock()方法。
// ReadLock#lock()
public void lock() {
sync.acquireShared(1);
}
// AQS#acquireShared(int arg)
// 如果竞争锁成功,直接返回
// 如果竞争失败,则去排队
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 竞争读锁
doAcquireShared(arg); // 竞争失败去排队
}
// ReentrantReadWriteLock.Sync#tryAcquireShared(int unused)
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 已加写锁
// 如果加写锁的线程不是当前线程,那么读锁也加不成,直接返回-1
// 否则,读写锁支持锁降级,加了写锁的线程可以再加读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 理论上讲,如果没有加写锁,不管有没有加读写锁,都可以去竞争读锁,毕竟读锁是共享锁
// 但是,存在两个特殊情况:
// 1. 对于公平锁来说,如果等待队列不为空,并且当前线程没有持有读锁(重入加锁),那么,线程就去排队。
// 2. 对于非公平锁来说,如果等待队列队首线程是写线程,那么线程就去排队。
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// readHolds是ThreadLocal变量,保存跟这个线程的读取重入次数
// 如果重入次数为0,表示没有加读锁,返回-1去排队
// 如果重入次数>0,表示已加读锁,可以继续重入,不用排队。
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 竞争读锁成功,更新线程重入次数
rh.count++;
}
return 1; // 成功获取读锁
}
// 上述的方式是为了让程序更快获取到写锁
// 如果获取不到则使用自旋的方式,再次尝试去竞争读锁。代码逻辑上述的类似。
return fullTryAcquireShared(current);
}
// AQS.doAcquireShared(),负责排队和等待唤醒。跟acquireQueued()函数类似,主要区别有两点
private void doAcquireShared(int arg) {
// 区别一:标记此线程等待的是共享锁。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final 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);
}
}
ReadLock#unlock实现原理如下:
// ReadLock#unlock()
public void unlock() {
sync.releaseShared(1);
}
// AQS#releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 释放读锁,当所有的读锁都释放成功,state=0
doReleaseShared(); // 唤醒等待队列中位于队首的线程
return true; // 释放成功
}
return false;
}
// ReentrantReadWriteLock.Sync#tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 以上代码,主要就是更新本线程对读锁的重入次数
// 因为有可能多个线程同时释放读锁,通过CAS更新state,所以通过自旋+CAS方式保证线程安全
for (;;) {
int c = getState();
// c-SHARED_UNIT; 相当于读锁的加锁次数-1
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // state == 0 才会返回true,才会去唤醒等待中的线程
}
}