Java中的锁
锁是用来控制多个线程访问共享资源的方式。
本章主要介绍了Java并发包中与锁相关的API和组件
Lock接口
Lock接口前,Java程序靠synchronized关键字实现锁功能。对比起来,Lock接口需要显式声明获取与释放,并且可以中断获取锁、超时获取锁。synchronized关键字将锁的获取或释放固化了。
特性
- 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
- 能被中断地获取锁:与
synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。 - 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回。
Lock方法
void lock()void lockInterruptibly() throws InterruptedExceptionboolean tryLock()boolean tryLock(long time, TimeUnit unit) throws InterruptedExceptionvoid unlock()Condition newCondition()
void lock()
获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回。
void lockInterruptibly() throws InterruptedException
可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock()
尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
void unlock()
释放锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
超时获取锁,当前线程在以下3种情况下会返回:
- 当前线程在超时时间内获得了锁
- 当前线程在超时时间内被中断
- 超时时间结束,返回
false
Condition newCondition()
获取等待通知组件,该组件和当前锁绑定,当前线程只是获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁
队列同步器 AbstractQueueudSynchronizer
用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队。
同步器主要使用方式是继承。
- getState()
- setState()
- compareAndSetState(int expect, int update) 同步组件:
- ReentrantLock
- ReentrantReadWriteLock
- CountDownLatch
队列同步器的接口与实例
同步器可重写的方法
protected boolean tryAcquire(int arg)protected boolean tryRelease(int arg)protected int tryAcquireShared(int arg)protected boolean tryReleaseShared(int arg)protected boolean isHeldExclusively()
protected boolean tryAcquire(int arg)
独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease(int arg)
独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg)
共享式获取同步状态,返回大于等于0的值,表示获取成功,反正获取失败
protected boolean tryReleaseShared(int arg)
共享式释放同步状态
protected boolean isHeldExclusively()
当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
同步器提供的模板方法
void acquire(int arg)独占获取同步状态void acquireInterruptibly(int arg)独占且响应中断获取同步状态,若获取不到则被中断boolean tryAcquireNanos(int arg, long nanos)在void acquireInterruptibly(int arg)上增加了超时限制void acquireShared(int arg)共享式获取同步状态,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态void acquireSharedInterruptibly(int arg)响应中断boolean tryAcquireSharedNanos(int arg, long nanos)增加超时限制boolean release(int arg)独占式释放锁,释放后将同步队列中第一个节点包含的线程唤醒boolean releaseShared(int arg)共享式释放同步状态Collection<Thread> getQueuedThreads()获取等待在同步队列上的线程集合
以上大致可分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。
自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
队列同步器实现
主要依靠:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法。
同步队列
- 节点状态
int waitStatusNode prevNode nextNode nextWaiterThread thread
独占式同步状态获取与释放
实现同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作
public final void acquire(int arg) {
if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 同步器的
addWaiter和enq方法
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;
}
}
}
}
- acquireQueued
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原则
node.prev = head && tryAcquire(args) - 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(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
共享式同步状态获取与释放
public final void acquireShared(int arg) {
if(tryAcquireShared(arg) < 0) doAcquireShared(arg);
}
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;
if(interrupted) selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if(failed) cancelAcquire(node);
}
}
- releaseShared方法
public final boolean releaseShared(int arg) {
if(tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法释放同步状态后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
独占式超时获取同步状态
通过调用同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
long lastTime = System.nanoTime();
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;
failed = false;
return true;
}
if(nanosTimeout <= 0) return false;
if(shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanosTimeout();
nanosTimeout -= now - lastTime;
lastTime = now;
if(Thread.interrupted()) throws new InterruptedException();
}
} finally {
if(failed) cancelAcquire(node);
}
}
自定义同步组件——TwinsLock
编写一个自定义同步组件来加深对同步器的理解。
设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock。
需求:
- 确定访问模式
- 定义资源数
- 组合自定义同步器
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if(count <= 0) {
throw new IllegalArgumentException("count must large than zero.")
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for(;;) {
int current = getState();
int newCount = current - reduceCount;
if(newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for(;;) {
int current = getState();
int newCount = current + returnCount;
if(compareAndSetState(current, newCount)) {
return true;
}
}
}
}
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
//其他方法略
}
重入锁
ReentrantLock支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。该锁还支持获取时的公平和非公平性选择。
实现重进入
需要解决两个问题:
- 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,释放自减,等于0则表示最终释放成功
ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取和释放同步状态的代码如下: - 获取
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) {
if(compareAndSetState(0, acquires)) {
}
}
}
- 释放
protected final boolean tryRelease(int release) {
int c = getState() - release;
if(Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();
boolean free = false;
if(c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平与非公平获取锁的区别
- 公平:FIFO
- 非公平:只要CAS设置同步状态成功,则表示线程获取了锁,如上面的
nonfairTryAcquire方法 公平锁的实现:tryAcquire
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;
}
非公平锁可能会使线程饥饿,但其上下文切换次数更少
读写锁 ReentrantLockReadWriteLock
特性
- 公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
- 重进入:比如读锁后可再次获取读锁,写锁可以再次获取写锁和读锁
- 锁降级:写锁+读锁,写锁释放,则降级为读锁
接口
int getReadLockCount():当前读锁被获取的次数,不等于获取读锁的线程数。如一个线程连续获取了n次读锁,占用的线程是1,但返回值为nint getReadHoldCount():当前线程获取读锁的次数,Java6加入到读写锁中,使用ThreadLocal保存当前线程获取的次数boolean isWriteLocked():判断写锁是否被获取int getWriteHoldCount():返回写锁被获取的次数
使用实例
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
static Lock r = rw1.readLock();
static Lock w = rw1.writeLock();
//获取一个key对应的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key,value);
} finally {
w.unlock();
}
}
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
实现
读写状态设计
按位切割使用,全32位,低16位记录写状态,高16位记录读状态。\
- 获取写状态
S & 0x0000FFFF(将高16位全部抹去 - 获取读状态
S>>>16(无符号补0右移16位
写锁的获取与释放
写锁是一个支持重进入的排它锁。
- 当前线程已获取写锁->增加写状态
- 当前线程已获取读锁或不是已经获取写锁的线程->进入等待状态
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");
setState(c + acquires);
return true;
}
if(writerShouldBlock() || compareAndSetState(c, c + acquires)) {
return false
}
setExclusiveOwnerThread(current);
return true;
}
读锁的获取与释放
protected final int tryAcquireShared(int unused) {
for(;;) {
int c = getState();
int nextc = c + (1 << 16);
if(nextc < c) throw new Error("Maximum lock count exceeded");
if(exclusiveCount(c) != 0 && owner != Thread.currentThread()) return -1;
if(compareAndSetState(c, nextc)) return 1;
}
}
锁降级
因为数据不经常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化则进行数据准备工作,同时其他处理线程被阻塞,直到当前线程完成数据准备工作:
public void processData() {
readLock.lock();
if(!update) {
//必须先释放读锁
readLock.unlock();
//锁降级从写锁开始
writeLock.unlock();
try {
if(!update) {
//准备数据流程略
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
//锁降级完成,写锁降为读锁
}
try {
//使用部分省略
} finally {
readLock.unlock();
}
}
update变量(布尔类型且由volatile修饰)被设置为false,此时所有访问processData()方法的线程都能感知到变化。
其中读锁的获取是为了保证数据的可见性,如果不获取而是直接释放写锁,假设其他线程在此时获取了写锁并修改数据,则当前线程无法感知到。
RentrantReadWriteLock不支持锁升级也是为了保证数据可见性。若多个线程获取读锁,而只有一个线程升级为写锁,则该写锁对其他已获取读锁的线程不可见。
LockSupport工具
用来阻塞或唤醒线程
void park():阻塞当前线程,只有使用unpark()或当前线程被中断才从park()方法返回void parkNanos(long nanos):阻塞当前线程,最长不超过nanos秒,返回条件在park()的基础上增加了超时返回void unpark(Thread thread):唤醒处于阻塞状态的线程thread
Condition 接口
提供类似Object类的监视器方法,与Lock配合可以实现等待/通知模式。需要调用Lock.lock()获取锁,调用Lock.newCondition()获取Condition对象
部分方法及描述
void await() throws InterruptedException该线程进入等待状态直到被通知(signal)或中断,当线程进入运行状态且从await()方法返回的情况,包括:
其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒
其他线程(调用interrupt()方法)中断当前线程 如果当前线程从该方法返回,表明该线程已经获取了Condition对象所对应的锁void awaitUninterruptibly()进入等待且对中断不敏感long awaitNanos(long nanosTimeout) throws InterruptedException进入等待直到被通知、中断或超时,被通知返回true,超时falsevoid signal()唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁void signalAll()唤醒所有等待在Condition上的线程。
实现
等待
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);
}
通知
public final void signal() {
if(!isHeldExclusively()) throw new IllegalMonitorStateException();
Node first = firstWaiter;
if(first != null) doSignal(first);
}