Java中的锁
- 重入锁(ReentrantLock)
- 读写锁(ReentrantReadWriteLock)
Java锁如何实现
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;//锁
abstract static class Sync extends AbstractQueuedSynchronizer {
//some code
}
//非公平锁
static final class NonfairSync extends Sync {
//some code
}
//公平锁
static final class FairSync extends Sync {
//some code
}
......
}
通过ReentrantLock的简化代码,我们可以发现它内部有一个内部类Sync对象来代表锁,而这个Sync对象继承了AQS。
队列同步器(AQS)
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
static final class Node {
//some code
}
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
......
}
由这个简化的AQS代码可以看出,最主要的属性就是一个内部类Node,和一个被volatile修饰的int变量state。
AQS通过使用state来表示同步状态,用内置同步队列来完成线程获取资源的排队管理工作。
AQS的主要使用方式为继承,子类通过继承AQS来管理同步状态,这样不可避免就要对同步状态进行操作,所以AQS提供了三个方法来操作同步状态,即getState()、setState()和compareAndSetState()。
AQS的实现分析
1.同步队列
AQS依赖于内部的一个同步队列(双向队列)来完成线程的管理。当一个线程获取同步状态失败后,同步器会将该线程、等待状态、前驱和后继节点等信息构造成一个Node节点,调用CAS方法compareAndSetTail(Node expect,Node update)将其放在同步队列的尾部,同时阻塞该线程。
同步队列的首节点是获取到同步状态的线程,当它释放同步状态后,会唤醒它的后继节点(线程),当后继节点获得了同步状态后就将自己设为首节点。
2.抢占式同步状态的获取和释放
获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
抢占式方法acquire的主要逻辑是:先调用tryAcquire去线程安全地尝试获取同步状态,如果失败了,就构造一个抢占式Node节点:Node.EXCLUSIVE,通过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)) {//尝试使用CAS将节点放在队列尾部
pred.next = node;
return node;
}
}
//失败了就执行enq,死循环尝试将节点放在队列尾部
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;
}
}
}
}
将节点成功放在队列尾部后,就调用acquiredQueue去自旋获取同步状态:
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; //将原来的头节点设为null方便垃圾回收
failed = false;
return interrupted;//返回false
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果线程被中断,则返回true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
注意上面的方法,正常情况下acquireQueued返回的是false,所以acquire方法中的selfInterrupt方法不会执行;但是如果当前线程被中断,acquireQueued返回的就是true,那么selfInterrupt就会执行,去中断线程。
释放
通过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去释放锁,成功之后就用unparkSuccessor()去唤醒后继节点。
3.共享式同步状态获取和释放
获取
通过调用同步器的acquireShared可以共享式地获取同步状态。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)//返回值>=0代表获取到了同步状态,返回值<0就需要执行doAcquireShared()方法自旋获取同步状态
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//构建一个共享式Node节点
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);
}
}
简而言之共享式获取的过程就是做一次或者自旋请求同步状态。
释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
共享式也是先释放同步状态,然后唤醒后继节点,唯一不同的是共享式需要通过CAS保证释放的线程安全。
重入锁ReentrantLock
重入锁即获得了锁的线程可以重复对资源加锁,还支持非公平锁和公平锁。
1.可重入的实现
ReentrantLock内部有个方法可以返回获得锁的线程,只要判断一下是否是当前线程就可以实现重入,重入一次就要增加一次同步状态的值,退出一次就要减少一次同步状态的值。
2.非公平锁的获取
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))//先尝试直接使用CAS获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//失败了就调用AQS的acquire方法
}
//ReentrantLock的tryLock方法就是调用这个方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
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) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//是则增加同步状态
return true;
}
return false;//没有获得同步状态则返回false
}
3.公平锁的获取
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);//直接调用AQS的acquire方法
}
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;
}
}
4.公平锁和非公平锁的释放
两者的释放都是同一个unlock方法,底层直接调用AQS的独占式release方法。
总结
公平锁和非公平锁最大的区别是在于tryLock方法的实现上。公平锁的tryLock是先查看同步队列中有没有等待的节点,有就直接退出方法返回false;非公平锁是直接用CAS去获取同步状态(因为刚释放完同步状态马上去获取的话,成功率是很高,在这里体现了非公平),获取失败了才退出方法返回false。
为什么非公平锁是默认的?
每当一个不同的线程获取同步状态,就相当于进行了1次上下文切换,假如有5个线程,每个线程要获取两次同步状态才能完成任务,那么公平锁就要进行10次上下文切换,而非公平锁因为基本上每个线程都可以连续获得同步状态,这样就只要进行5次上下文切换,大大节省了系统开销。所以一般情况下非公平锁的效率比较高,假如能够确定需要使用公平锁,就用ReentrantLock(true)将重入锁设置为公平锁就可以了。
读写锁(ReentantReadWriteLock)
读写锁也是可重入的,也支持公平锁和非公平锁。
1.读写状态和可重入的设计
在AQS中用state来表示同步状态,重入锁也遵循了AQS的设计,但是读写锁它进行了优化,用一个state变量表示读状态和写状态。具体实现如下:
一个int型的state有32位,读写锁用高16位来表示写状态,记录写锁的次数;用低16位表示读状态,记录读锁的次数。
2.读写锁的设计
写锁的获取和释放
写锁的lock方法是直接调用AQS的acquire方法,在此不再介绍。
public void lock() {
sync.acquire(1);
}
写锁的tryLock是一个支持重入的排他锁方法,只要写状态为0或者获得写锁的就是当前线程,且读状态也为0,该线程就能获取写锁,增加写状态。假如写状态不为0且当前线程不是获得写锁的线程,或者读状态不为0,则线程进入阻塞状态。
public boolean tryLock( ) {
return sync.tryWriteLock();
}
//Sync类的方法
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {//说明写状态或者读状态不为0
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())//如果是读状态不为0或者获得写锁的不是当前线程,就返回false
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
为什么读状态必须为0呢?(即为什么在读锁被占用的时候不能获得写锁)
这是因为读写锁必须保证写锁对数据的操作对读锁是可见的。假如有线程获得了读锁,然后给另一个线程获得写锁,那么获得读锁的线程就无法得知获得写锁的线程对数据的操作。只有当所有的线程都释放了读锁,才允许线程获得写锁去改变数据。
写锁的释放是直接调用AQS的release方法来实现的。
public void unlock() {
sync.release(1);
}
读锁的获取与释放
public void lock() {
sync.acquireShared(1);
}
可以看出读锁的lock方法是调用AQS的共享式同步状态获取方法acquireShared来实现的。
读锁的tryLock方法是调用Sync的tryReadLock方法来实现的。假如在获取读锁的过程中,有其他线程获取了写锁,就退出方法并返回false,否则就死循环尝试用CAS增加读状态。
public boolean tryLock() {
return sync.tryReadLock();
}
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {//死循环尝试增加读状态
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)//如果写状态不为0且获得写锁的线程不是当前线程,返回false
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
读锁的释放采用的是AQS的共享式同步状态释放方法。
public void unlock() {
sync.releaseShared(1);
}
LockSupport
AQS同步队列中线程的阻塞和唤醒是如何实现的呢?
LockSupport提供了多个阻塞和唤醒线程的方法:
- park():阻塞当前线程,被唤醒或者中断才能从park方法返回
- parkNanos(long nanos):阻塞当前线程一段时间,超时、唤醒或中断才能返回
- parkUntil(long deadline):阻塞当前线程,直到deadline这一时刻、唤醒或者中断才能返回
- unpark():唤醒阻塞的线程
Condition接口
Object拥有一组监视器方法包括wait、sleep、notify等,可以与synchronized配合使用。Condition接口同样定义了一组监视器方法用于和Lock配合使用。方法列表如下:
- await():跟wait方法差不多,都需要得到通知才能恢复运行,但是还能被其他线程调用interrupt()方法中断,如果等待线程从await方法返回,说明该线程获取到了Condition对象所对应的锁
- awaitUninterruptibly():同await,但是不会被中断
- awaitNanos():同await,可以被通知、中断和超时
- awaitUntil():同awaitNanos,如果没到时就被通知或中断,则返回true,否则如果到时自动结束则返回false
- signal():唤醒一个等待在Condition上的线程,该线程从等待方法返回必须获得与Condition相关的锁
- signalAll():唤醒所有等待线程,能够从等待方法返回的线程必须获得与Condition相关的锁
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
conditionAwait();
}
}).start();
Thread.sleep(1000);
lock.lock();//因为await释放了锁,这里才能获取到锁
condition.signal();//唤醒等待线程
lock.unlock();//释放锁
}
public static void conditionAwait() {
lock.lock();
try {
condition.await();//必须获得锁才能调用await,调用完await后锁就被释放了
System.out.println("被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition的实现
AQS中的内部类ConditionObject类实现了Condition接口。
1.等待队列
ConditionObject用于构建等待队列的节点就是AQS的Node。所有调用await方法的线程都会被构建成Node节点放入等待队列。因为只有获得锁的线程才能调用await,所以这里是线程安全的。
2.等待await
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();//这里将当前线程加入等待队列
int savedState = fullyRelease(node);//释放锁
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
3.唤醒signal
public final void signal() {
if (!isHeldExclusively())//判断当前线程是否获得了锁,必须获得锁才能调用signal方法
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);//唤醒首节点
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);//将节点放入同步队列尾部
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);//唤醒节点
return true;
}
唤醒线程其实是用enq方法将首节点线程从等待队列中移到同步队列尾部去,然后调用LockSuppor.unpark唤醒线程。
注意等待队列和同步队列之间的差别:
等待队列存放的是所有调用await的线程,进入等待状态的线程是挂起的,无法获得CPU时间片。而同步队列中的线程处于RUNNABLE状态,是不断自旋的。所以在将等待队列的节点移到同步队列后,还要用LockSupport.unpark去唤醒节点,让它进入RUNNABLE状态,它才能去获取CPU时间片。