第5章 Java中的锁

前言

本章总结了Lock接口,队列同步器(AbstractQueuedSynchronizer),可重用锁ReentrantLock

第5章 Java中的锁

一、Lock接口

显式获取/释放锁,具有可操作性、可中断的获取锁,以及超时获取锁等功能。(相比synchronized关键字隐式锁,缺失了点隐式锁的便捷性,但功能可操作性更强。synchronized虽然可以隐式、简化锁管理,但是固化了操作,缺乏可扩展性)

基本使用:

1
2
3
4
5
6
Lock lock = new ReentrantLock();	// 声明可重用锁
lock.lock(); // 获取锁
try { // 不要将获取锁卸载try块中,避免异常抛出时被无故释放锁
} finally {
lock.unlock(); // 🔺 在finally中释放锁,保证锁获取之后能被释放
}

主要特性:(除了普通自旋的获取锁外,其他的特性)

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没被其他线程占有,则成功获取并持有锁(此方式将只获取依次,在获取时刻内立即返回,不进行自旋)
可被中断地获取锁 当获取到锁的线程被中断时,抛出中断异常,并释放锁(与synchronized不同)
超时获取锁 在指定时间前获取锁,否则无法获取锁,自动返回

Lock接口的API:

方法名 描述
void lock() 自旋地获取锁,直到获取后才返回
void lockInterruptibly() throws InterruptException 可响应中断的获取锁,在锁获取的过程中可以中断当前线程。
boolean tryLock() 尝试非阻塞的获取锁调用后立即返回,成功获取则返回true,否则返回false
boolean tryLock(long time, TimeUni unit) throws InterruptException 超时获取锁,在以下情况返回
① 当前线程在超时时间内获取锁
② 当前线程在超时时间内被中断
③ 超时结束,失败返回false
void unlock() 释放锁
Condition newCondition() 获取等待通知的组件,该组件和当前的锁绑定。当前线程只有获得了锁,才能调用该组件的wait() 方法,调用后,当前线程释放锁。

Lock接口的实现一般都是通过聚合使用一个同步器AbstracQueuedSynchronizer的子类实现访问控制的。

Lock实现

二、队列同步器AbstractQueuedSynchronizer

队列同步器(简称同步器)AbstractQueuedSynchronizer

是用来构建锁和同步组件的基本框架,使用一个volatile的int表示同步状态,使用一个FIFO队列完成获取资源的线程的排队工作。

同步器主要使用方式:继承

子类继承同步器定义为自定义同步组件的静态内部类,通过调用已提供的方法来实现其抽象方法来管理同步状态。

同步器已经提供了3个方法来访问和修改同步状态供子类使用:

  • getState()获取当前同步状态
  • setState(int newState)设置当前同步状态
  • compareAndSetState( int expect, int update )使用CAS设置当前状态,该方法保证状态设置的原子性

同步器可重写的方法:(包括独占式获取/释放同步状态)

方法名 描述 备注
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() 当前同步器是否线程被独占

同步器提供的模板方法:

大致分为3类:

  • 独占式获取与释放同步状态:acquire/release(另含aquireInterruptibly响应中断操作和tryAcquireNanos超时等待操作)
  • 共享式获取与释放同步状态:acquireShared/releaseShared(另含aquireSharedInterruptibly响应中断操作和tryAcquireSharedNanos超时等待操作)
  • 查询同步队列中的等待情况Collection<Thread> getQueuedThreads()

利用同步器实现的独占锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Mutex implements Lock {
// 使用静态内部类 实现 自定义同步器
// 这样就屏蔽了同步器的操作,用户只需要使用Mutex包装同步器的方法
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1; // getState()
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) { // CAS操作
setExclusiveOwnerThread(Thread.currentThread()); // 设置拥有者线程
return true;
}
return false;
}
// 释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new // 若没有被占用(同步状态为0)则抛出异常
IllegalMonitorStateException();
setExclusiveOwnerThread(null); // 否则被占用了,就设置拥有者线程为空
setState(0); // 然后置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));
}
}

2.2. 队列同步器的内部实现

实现机制主要包括:同步队列、独占式同步状态获取和释放、共享式同步状态获取和释放。

2.2.1. 同步队列

  • 同步队列是一个FIFO双向队列,每个节点保存前驱和后继节点以及对应线程。

  • 同步队列存放获取同步状态失败的线程

  • 当同步状态释放时,唤醒首节点,后面的节点的唤醒通过前驱节点出队或被中断实现。
  • 插入队列使用CAS确保原子性(因为可能会有并发插入场景),而队列头节点出队不需要CAS(因为出队只有一个线程能够获取同步状态)。
  • 节点在同步状态中会自旋检查同步状态,但只有前驱节点是头节点时才能尝试获取同步状态。
  • 移出队列(停止自旋)的条件是:前驱节点是头节点且成功获取了同步状态。

节点自旋获取同步状态

2.2.2. 独占式获取同步状态

流程:(同一时刻只能有一个线程访问同步状态)

独占式获取同步状态流程

2.2.3. 共享式同步状态:

同一个时刻能有多个线程同时获取到同步状态(如读写文件时,可多进程同时读1个文件,此时写被阻塞;当文件在写入时,所有读和其他写被阻塞)写为独占,读为共享。

  • 使用共享式同步状态的并发组件必须使用CAS确保释放资源的原子性

锁和同步器的关系

同步器是实现锁的关键。

锁是面向使用者的,定义了使用者和锁之间的接口,隐藏了实现细节。同步器面向锁的实现者,简化锁的实现方式,隐藏了同步状态管理、线程排队、等待唤醒的操作。

三、 重入锁ReentrantLock

ReentrantLock重入锁:支持重进入的锁,表示该锁支持一个线程对资源重复加锁。有公平性和非公平性选择。

重进入:已经获取到锁的线程能够再次调用lock()而不被锁阻塞。

  • 其实synchronized关键字隐式支持了重进入(如synchronized修饰的递归方法)

公平性:等待时间最长的线程优先获取锁(请求先到先得FIFO,符合请求的绝对时间顺序)

非公平性:就是不公平性

  • 公平虽然能减少“饥饿”,但效率其实没有非公平高,因为会造成大量的线程切换开销代价。
  • 非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了更大的吞吐量

实现重进入

线程再次获取锁:识别当前线程是否为当前占据锁的线程,是则成功获取。

锁的最终释放:重复n次获取锁后,再释放n次时,其他线程能获取到该锁。

非公平锁和公平锁的实现细节

非公平锁的实现细节:

获取锁需要CAS原子操作(非公平情况下会产生并发问题)

释放锁时不存在并发,不需要CAS

没有队列上的严格限制

公平锁的实现细节:

多一个判断当前线程是否是头节点,是头节点才能获得锁(严格的FIFO)

可以看到:公平锁每次都会从队列中的第一个节点获取到锁,大量的线程切换;而非公平锁出现了一个线程连续获取锁的情况。

这是因为当一个线程请求锁时,只要获取了同步状态即成功获取锁,在此前提下,刚释放锁的线程再次获取同步状态的几率很大。

三、读写锁ReentrantReadWriteLock

读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

(读写锁能够简化线程交互场景的编程方式,如生产者-消费者)

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

Java提供的ReetrankReadWriteLock有三个特性(其实大概比重入锁多了锁降级):

特性 说明
公平性选择 支持非公平和公平的锁获取方式
重进入 读锁或写锁都支持重进入
锁降级 获取写锁—>获取读锁—>释放写锁
以此完成写锁降级为读锁

读写锁的接口示例

ReadWriteLock接口仅定义了读锁和写锁两个方法:

方法名 描述
readLock() 获取读锁
writeLock() 获取写锁

其实现ReetrantReadWriteLock还提供了外界监控锁状态的方法:

方法名 描述
int getReadLockCount() 返回当前读锁被获取的次数(不等于获取读锁的线程数)
int getReadHoldCount() 返回当前线程获取读锁的次数(参数保存在ThreadLocal中)
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //声明读写锁
static Lock r = rwl.readLock(); // 获取读锁r
static Lock w = rwl.writeLock(); // 获取写锁w
// 读方法:获取一个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(); // 释放写锁
}
}
}

读写状态设计

读写锁使用一个int变量(32位),维护多个读线程和写线程的状态

写状态:被一个线程重复获取写锁的次数;

读状态:是所有线程获取读锁次数的总和

按位切割方法:高16位表示读,低16为表示写(按2进制16位数计算锁获取次数)

推论:同步状态S不为0时,写状态为0时时,则读状态(S >>> 16) 定大于 0,即读锁被获取

1
读锁被获取判断:((S != 0) && (S & 0x0000FFFF == 0)) => ((S >>> 16) > 0)

读写锁锁状态的划分方式

锁降级

获取写锁—>获取读锁—>释放写锁

好处:锁降级是为了保证数据的可见性,如果不是锁降级而是释放写锁再获取读锁,中途可能会被别的线程写打断下一步读。锁降级同时也可以保证其他读线程能获取读锁,达到并发读的目的。

LockSupport工具

LockSupport也是构建同步组件的基础工具之一,提供了一组另线程阻塞和唤醒功能的公共静态方法

park(Object blocker) 用于在某对象上阻塞当前线程(此外还有超时等待方法xxNanos和xxxUntil)

unpark(Thread th) 方法唤醒一个被阻塞的线程

Condition接口

Condition接口实现的功能与Object监视器的等待/通知机制类似(await/signal - wait/notify)

Condition是同步器AbstractQueuedSynchronizer的内部类

调用Condition需要先获取其关联的Lock锁(一个Condition必须由Lock的newCondition()方法创建出来) 使用前需获取lock锁

Condition部分方法:

方法名 描述
void await() throws InterruptedException 可被中断地等待,直到被signal通知
void awaitUninterruptibly() 不响应中断的等待,直到被signal通知
long awaitNanos(long nanosTimeOut) throws InterruptedException 超时等待
boolean awaitUntil(Date deadline) throws InterruptedException 等待到某个时间,中途被通知返回true,否则返回false。响应中断
void signal() 唤醒一个等待在Condition上的线程,该线程返回前必须获得Condition相关联的锁
void signalAll() 唤醒所有等待在Condition上的线程

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Lock lock = new ReetrantLock();		// 创建锁实例
Condition condition = lock.newCondition(); // 获取一个Condition对象

public void conditionWait() throws InterruptedException {
lock.lock(); // 1. 获取锁lock()
try {
condition.await(); // 2. 当前线程等待(condition可在获得锁状态下操作)
} finally {
lock.unlock(); // 3. 释放锁
}
}

public void conditionSignal() throws InterruptedException {
lock.lock(); // 1. 获得锁
try {
condition.signal(); // 2. 唤醒1个等待在condition上的线程
} finally {
lock.unlock(); // 3. 释放锁
}
}

对比Object监视器和Condition

对比项 Object Monitor Methods Condition
前置条件 获取对象锁(synchronized) 调用Lock.lock()获取锁
调用Lock.newCondition()获取Condition对象
调用方式 直接在object对象上调用
object.wait()
直接使用condition调用
如:condition.await()
等待队列个数 一个 多个(一个Condition本身维护一个等待队列,一个同步器上有多个Condition和一个同步队列)
当前线程释放锁并进入等待 支持(wait) 支持(await)
线程释放锁后,在等待中不响应中断 不支持 支持
线程释放锁进入超时等待 支持 不支持
线程释放锁等待到将来一时间 不支持 支持
唤醒等待队列中一个或全部线程 支持 (notify) 支持 (signal)

Condition等待队列和同步器的联系

Condition是同步器AbstractQueuedSynchronizer的内部类,同步器维护的多个等待队列和一个同步队列,而每个等待队列都是由每个Condition维护的,也是存放着获得锁失败的线程。

signalAll相当于该Condition上的每一个节点均执行以此signal()方法,所有节点移到同步队列中,并唤醒它们。

1532519358919

本文标题:第5章 Java中的锁

文章作者:Aaron.H

发布时间:2018年05月06日 - 15:05

最后更新:2018年09月18日 - 17:09

原始链接:https://uncleaaron.github.io/Blog/Java/Java并发编程艺术/第5章-Java中的锁/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。