文章目录
1 AQS (AbstractQuenedSynchronizer)
- 基于 AQS 的同步器类,包含“获取”和“释放”操作(“获取”需要依赖状态,通常会阻塞;“释放”不会阻塞)
- 对于
ReentrantLock
和Semaphore
,“获取”和“释放”比较直观 - 对于
CountDownLatch
,“获取”表示“等待直到闭锁到达结束状态”,即countDownLatch.await()
- 对于
FutureTask
,“获取”表示“等待直到任务完成”,即futureTask.get()
- 对于
boolean acquire() throws InterruptedException {
while (当前状态不允许获取) {
if (需要阻塞获取请求) {
将当前线程加入队列并阻塞
} else {
return false;
}
}
更新同步器状态
如果当前线程位于队列,将线程移出队列
return true;
}
void release() {
更新同步器状态
if (当前状态允许某些被阻塞的线程获取) {
解除队列中若干个线程的阻塞状态
}
}
AQS 的主要数据结构
- 状态
volatile int state
ReentrantLock
将它作为锁的重入次数(因此ReentrantLock
还要记录锁的持有者)Semaphore
将它作为剩余的许可数量FutureTask
用它表示任务的状态:尚未开始、正在运行、已完成、已取消CountDownLatich
将它作为计数值
- 同步队列
- 存放等待获取锁的线程
- 条件队列
- 存放等待被唤醒的线程,线程被唤醒后,加入到同步队列的队尾
- 与
java.util.concurrent.locks.Condition
接口相关联的队列,相关方法await()/signal()/signalAll()
,它允许更细粒度的线程间协调
和synchronized的实现相比,Monitor包含的数据结构:
- owner:当前拥有锁的线程
- count:重入次数
- EntryList:抢夺锁的线程队列
- WaitSet:等待被 notify()/notifyAll() 唤醒的线程队列
- 线程通过 CAS 改变状态符
state
,成功则获取锁成功,失败则进入等待队列,等待被唤醒 - AQS 采用 自旋锁 的机制
2 Lock 接口与显式条件
public interface Lock {
/**
* 阻塞直到加锁成功
**/
void lock();
/**
* 解锁
**/
void unlock();
/**
* lock方法的响应中断版本:阻塞直到加锁成功或被中断
**/
void lockInterruptibly() throws InterruptedException;
/**
* 非阻塞(调用后立即返回),成功获取锁返回true,否则返回false
**/
boolean tryLock();
/**
* tryLock的延时等待版本:等待时间内可以响应中断,如果发生中断则抛出异常
**/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 新建一个显式条件
**/
Condition newCondition();
}
Condition
接口的关键方法:await()
和signal()/signalAll()
- 类比
wait()
和notify()/notifyAll()
- 调用
await()/signal()
要先加锁,否则会抛出异常(类比wait()/notify()
要在对应的同步代码块中调用) - 同样,从
await()
返回后,线程重新获得锁,但需要检查等待条件(因此总是在循环中检查条件,调用await()
)
- 类比
- 显式条件和显式锁配合,
synchronized
和wait/notify
配合 - 类比
wait()/notify()
的 Demo
static void awaitSignalTest() throws InterruptedException {
WaitThreadWithLock thread = new WaitThreadWithLock();
thread.start();
Thread.sleep(2000L);
thread.setFlagTrue(); // 主线程调用setFlagTrue()
}
class WaitThreadWithLock extends Thread {
private boolean flag = false;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
@Override
public void run() {
lock.lock();
try {
while (!flag) {
try {
System.out.println("sub thread call await()");
condition.await();
System.out.println("sub thread return from await()");
} catch (InterruptedException e) {
System.out.println("await interrupted");
}
}
} finally {
lock.unlock();
System.out.println("sub thread terminate");
}
}
public void setFlagTrue() { // 由主线程调用
lock.lock();
try {
flag = true;
condition.signal();
} finally {
lock.unlock();
System.out.println("main thread call signal()");
}
}
}
结果:
sub thread call await()
main thread call signal()
sub thread return from await()
sub thread terminate
3 转账 Demo:解决死锁的两种方案
- 账户类定义(不考虑余额不足的情况)
class BankAccount {
private final long uid;
private volatile long balance;
private final Lock lock = new ReentrantLock();
public BankAccount(long uid, long initBalance) {
this.uid = uid;
this.balance = initBalance;
}
public void add(long money) {
accountTryLock();
try {
balance += money;
} finally {
accountUnlock();
}
}
public void reduce(long money) {
accountTryLock();
try {
balance -= money;
} finally {
accountUnlock();
}
}
public boolean accountTryLock() {
return lock.tryLock();
}
public void accountLock() {
lock.lock();
}
public void accountUnlock() {
lock.unlock();
}
public long getUid() {
return uid;
}
public long getBalance() {
return balance;
}
}
- 转账方案1,无同步措施
/**
* 无同步转账
*/
static void naiveTransfer(BankAccount from, BankAccount to, long money) {
from.accountLock();
try {
to.accountLock();
try {
from.reduce(money);
to.add(money);
} finally {
to.accountUnlock();
}
} finally {
from.accountUnlock();
}
}
- 转账方案2,固定加锁顺序,先获取 id 更小的账户的锁
/**
* 确定加锁顺序的转账
*/
static void transferWithOrder(BankAccount from, BankAccount to, long money) {
// 按照uid指定加锁顺序,先获取uid更小的账户的锁
BankAccount first = from.getUid() < to.getUid() ? from : to;
BankAccount second = from.getUid() > to.getUid() ? from : to;
first.accountLock();
try {
second.accountLock();
try {
from.reduce(money);
to.add(money);
} finally {
second.accountUnlock();
}
} finally {
first.accountUnlock();
}
}
- 转账方案3,使用
tryLock()
转账- 需要注意的是,每执行一次
transferWithTryLock
不一定成功转账(因为tryLock()
没有获取锁时,直接从方法返回)
- 需要注意的是,每执行一次
/**
* 使用tryLock的转账
*/
static boolean transferWithTryLock(BankAccount from, BankAccount to, long money) {
if (from.accountTryLock()) {
try {
if (to.accountTryLock()) {
try {
from.reduce(money);
to.add(money);
return true;
} finally {
to.accountUnlock();
}
}
} finally {
from.accountUnlock();
}
}
return false;
}
4 ReentrantLock 非公平锁加锁流程
- 非公平锁是指新来的线程跟 AQS 同步队列头部的线程竞争锁,队列其他的线程还是正常排队
- 公平锁严格执行 FIFO,新线程只能加入队尾
- 非公平锁尝试加锁,即执行
tryAcquire()
的流程是:检查state字段,若为0,表示锁未被占用,尝试占用锁;若不为0,检查当前锁是否被自己占用,若被自己占用,则更新 state 字段,重入次数加 1 - 如果以上两点都没有成功,则获取锁失败,进入同步队列
- 进入同步队列的线程尝试获取锁(最靠前的线程才有资格尝试),如果获取成功则成为队列新的头节点,获取失败则尝试挂起
- 线程入队后能够挂起的前提是,它的前驱节点的状态为 SIGNAL,状态为 SIGNAL 的节点在出队后会唤醒后面紧邻的节点
5 ReentrantLock 和 synchronized 的异同
- 都提供了互斥性和内存可见性
- 优先使用 synchronized,不满足要求时考虑 ReentrantLock
- 响应中断
- 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可以让它中断自己或者在别的线程中中断它
Lock
等待锁过程中可以用interrupt()
来中断等待Lock
接口提供的定时加锁方法tryLock(time, timeUnit)
和lockInterruptibly()
具有响应中断的能力
- 超时等待
- 规定超时等待时间,避免线程无限期的等待获取锁
Lock
接口提供的定时加锁方法tryLock(time, timeUnit)
- 公平锁与非公平锁
- 公平锁是指多个线程同时尝试获取同一把锁时,按照线程达到的先后顺序获取锁
- 而非公平锁则允许线程“插队”,新线程和队首的线程竞争锁
- 自动释放
- 内置锁以代码块为单位加锁,离开同步代码块自动释放锁
Lock
接口必须在finally
块中释放锁,而不会自动释放
private final Lock lock = new ReentrantLock(); public void test() { lock.lock(); try { // ... } finally { lock.unlock(); } } ```
- 设计思维
synchronized
是一种阻塞式算法,线程得不到锁的时候进入锁等待队列,等待其它线程唤醒,有上下文切换开销- 基于 CAS 的算法(AQS、原子变量等)是非阻塞的,如果发生更新冲突只是返回失败,不会阻塞,没有上下文切换的开销
6 ReentrantReadWriteLock
ReadWriteLock
接口暴露两个锁对象,读不互斥,写互斥,读写互斥- 这种锁设计旨在提高读操作的并发性,同时保证写操作的独占性,尤其适用于读多写少的场景
- 支持锁降级,即线程可以先获取写锁,然后获取读锁,最后释放写锁
- 读锁和写锁都支持线程重入
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
- 用读写锁包装 Map
class ReentrantReadWriteLockMap<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public V put(K key, V value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
public V get(K key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
}