Java并发包中锁原理剖析
概述
Java 5之后新增了Lock接口,自定义类可实现Lock接口,并通过内部静态类继承AQS抽象类的方式实现独占锁、共享锁。
锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。用户调用子类实现的Lock接口中提供的方法,而这些方法又调用同步器的方法来实现具体的功能。
AQS实现原理
AQS是一个Java提供的底层同步工具类,用一个int类型的变量state表示同步状态,并使用CAS操作来管理这个同步状态。同时,它还实现了一个FIFO的队列,底层采用双向链表实现,并有head和tial指针指向头和尾。
当一个线程获取到锁之后,通过CAS将state设置为1代表线程获取到锁;如果这时候有其它的线程在竞争锁,那么在失败后其它将会被加入队列尾部,并且自旋判断其前驱节点为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程设置为头节点,如果不是,则用LockSupport.park(this)将当前线程挂起 ,等待前驱节点释放unpark唤醒自己。
ReentrantLock实现原理
ReentrantLock是可重入锁,通过判断上次获取锁的线程是否为当前线程(current == getExclusiveOwnerThread()),如果是则可再次进入临界区并且增加同步状态值返回最后true,如果不是,则返回false。当释放锁时也要减小同步状态值。
ReentrantLock可实现公平锁,通过构造传参的方式。在非公平锁的基础上加入了对同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
Lock
锁是用来控制多个线程访问共享资源的方式。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口来实现锁功能,它提供了synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。
- Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- Lock接口的使用
Lock lock = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.ublock();
}
-
Lock接口的API
-
Lock接口与synchronized关键字的区别
AQS
AQS是AbustactQueuedSynchronizer(队列同步器)的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。
AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
AQS中可重写的方法分为独占式与共享式。
- AQS提供的可重写方法
实现自定的同步组件时,将会调用AQS提供的模板方法,这些模板方法内部又将调用上面重写的tryAcquire、 tryRelease、 tryAcquireShared 、tryReleaseShared isHeldExclusively方法。
- 模板方法描述
实现
同步队列
同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,获取锁失败那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
使用CAS将节点插入到尾部,并用tail指向该结点。
独占锁的获取和释放流程
获取
- 调用入口方法acquire(arg)
- 调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
- 将当前线程构造成一个Node节点,并利用addWaiter(Node node) 将其加入到同步队列尾部
- 调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态
- 自旋时,首先判断其前驱节点为头节点且释放&是否成功获取同步状态,两个条件都成立,则将当前线程的节设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待前驱节点释放唤醒自己,之后继续判断
释放
- 调用入口方法release(arg)
- 调用模版方法tryRelease(arg)释放同步状态
- 利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点
共享锁的获取和释放流程
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态
获取锁
- 在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态
- tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0
- 在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出
释放锁
- 调用releaseShared(arg)模版方法释放同步状态
- 调用模版方法tryReleaseShard(arg)释放同步状态
- 如果释放成功,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点
- 与独占式区别在于线程安全释放,通过循环和CAS保证,因为释放同步状态的操作会同时来自多个线程
独占锁和共享锁在实现上的区别
- 独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
- 共享锁的同步状态>1,取值由上层同步组件确定
- 独占锁队列中头节点运行完成后释放它的直接后继节点
- 共享锁队列中头节点运行完成后释放它后面的所有节点
- 共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
重入锁
重入锁指的是当前线程成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中ReentrantLock和synchronized都是可重入锁,synchronized由JVM偏向锁实现可重入锁,ReentrantLock可重入性基于AQS实现。
重入锁的基本原理是判断上次获取锁的线程是否为当前线程(current == getExclusiveOwnerThread()),如果是则可再次进入临界区,如果不是,则阻塞。
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//通过AQS获取同步状态
int c = getState();
//同步状态为0,说明临界区处于无锁状态,
if (c == 0) {
//修改同步状态,即加锁
if (compareAndSetState(0, acquires)) {
//将当前线程设置为锁的owner
setExclusiveOwnerThread(current);
return true;
}
}
//如果临界区处于锁定状态,且上次获取锁的线程为当前线程
else if (current == getExclusiveOwnerThread()) {
//则递增同步状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放 同步状态时减少同步状态值
公平锁和非公平锁
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点
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;
}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
读写锁
Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
写锁的获取与释放
写锁是一个独占锁。
ReentrantReadWriteLock中tryAcquire(arg)的实现:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
- 获取同步状态,并从中分离出低16为的写锁状态
- 如果同步状态不为0,说明存在读锁或写锁
- 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
- 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
- 如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入) 将当前线程设置为写锁的获取线程
写锁的释放过程与独占锁基本相同:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。
读锁的获取与释放
读锁是一个共享锁.
获取读锁的步骤如下:
- 获取当前同步状态
- 计算高16为读锁状态+1后的值
- 如果大于能够获取到的读锁的最大值,则抛出异常
- 如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
- 如果上述判断都通过,则利用CAS重新设置读锁的同步状态
读锁的释放步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。
使用AQS与Lock自定义一个锁
class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
流程:
- 这个自定义类Mutex首先实现了Lock接口
- 内部静态类Sync继承了AQS抽象类,并重写了独占式的tryAcquire和tryRelease方法
- 接着Mutex实例化Sync内部类
- Mutex类重写Lock接口的方法,如lock、tryLock、unlock等方法,具体实现是通过调用Sync类中的重写的方法(tryAcquire)以及模板方法(acquire)等
- 用户使用Mutex时调用Mutex提供的方法,在Mutex的实现中,调用同步器的模板方法acquire(int args)