学习资料
《Java并发编程的艺术》第5章 5.4~5.6
1.读写锁简介
读写锁与排他锁:
- 排他锁:同一个时刻只能运行一个线程进行访问(不管读写),如ReentrantLock
- 读写锁:同一时刻允许有多个读线程访问,但是有写线程访问时,所有其他线程(不管读写)都被阻塞
Java并发包提供的读写锁的实现是ReentrantReadWriteLock
,支持公平性选择,重入锁以及锁降级
- 锁降级:获取写锁—>获取读锁—>释放写锁,写锁能够降级为读锁
ReadWriteLock
接口仅提供了两个与锁相关方法:
lock readLock()
:获取读锁lock writeLock()
:获取写锁
ReentrantReadWriteLock
实现提供了四个展示内部工作状态的方法:
getReadLockCount()
:返回当前读写锁被获取的次数(不等于获取读锁的线程数,有重入)getReadHoldCount()
:返回当前线程获取读锁的个数(线程获取多个锁或重入)isWriteLocked()
:判断写锁是否被获取getWriteHoldCount()
:返回当前写锁被获取的次数(重入次数)
简单使用方式:
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
r.lock();r.unlock(); //读锁操作
w.lock();w.unlock(); //写锁操作
2.读写锁实现
2.1 读写状态设计
读写锁同样依赖自定义同步器来实现同步功能,读写状态就是同步器的同步状态
同步状态是一个整型(32位),需要维护多个读线程和一个写线程的状态,用高位16位表示读状态,低位16位表示写状态:
2.2 写锁获取与释放
写锁获取
写锁是一个支持重进入的排他锁:
- 如果当前线程已经获得了写锁,则增加写状态,重入锁
- 如果读线程已经被获取或者其他线程获取了写锁,则线程进入等待状态
- 如果读锁没有被获取,写锁也没有被获取,则当前线程获取写锁
读读共享,读写互斥,写写互斥
只有当读线程都释放了读锁,写锁才能被当前线程获取;写锁一旦被获取,后续的所有读写线程都被阻塞。
写锁释放
每次释放均减少写状态,写状态为0则真正被释放
2.3 读锁获取与释放
读锁获取
读锁是一个支持重进入的共享锁,能被多个线程同时获取,没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取
读状态是所有线程保存的所有锁的总数,也就是说新线程获取读锁会添加读状态,同一个线程锁重入也会添加读状态。Java6之后将每个线程获取的读锁次数存放在ThreadLocal中,使得读锁获取实现变得复杂
特殊情况:
- 其他线程获取写锁—>当前线程无法再获取读锁
- 当前线程获取写锁—>当前线程可获取读锁,之后再释放写锁(锁降级)
读锁释放
读锁每次释放(需要保证线程安全)都会减少读状态(减少1<<16
)
2.4 锁降级
写锁—>读锁
把持住(当前线程拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
示例代码:
public void processData() {
//获取读锁
readLock.lock();
if (!update) {
// 必须先释放读锁
readLock.unlock();
// 锁降级从写锁获取到开始
writeLock.lock();
try {
if (!update) {
// 准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
}
try {
// 使用数据的流程(略)
} finally {
readLock.unlock();
}
}
锁降级为了保证数据的可见性,防止写锁切换为读锁过程中被其他写锁获取到线程修改数据(此时修改的数据还未被使用)
ReentrantReadWriteLock不支持锁升级
3.LockSupport工具
LockSupport提供了最基本的线程阻塞和唤醒功能,内部调用了Unsafe类中的本地方法来实现,LockSupport是构建同步组件的基础工具
LockSupport定义了一组以park
开头的方法阻塞当前线程,以及unpark(Thread thread)
方法唤醒一个被阻塞的线程:park()
,parkNanos()
,parkUntil()
,unpark()
Java6增加了带blocker参数的park开头的方法如:park(Object blocker)
,blocker表示当前线程所在的阻塞对象,增加这些方法主要是为了问题排除和系统监控
4.Condition接口简介
Java提供了一组监视器方法(Object类上),主要包括wait()
,wait(long timeout)
,notify()
,notifyAll()
方法,这些方法和Synchronized关键字配合,可以实现等待/通知模式。
Condition接口也提供了类似Object监视器的功能,和Lock配合可以实现等待/通知模式。
Object监视器方法与Condition接口对比:
Condition的简单使用方式:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); //可创建多个condition
public void conditionWait() throws InterruptedException {
lock.lock();
try {
//相当于obj.wait(),在Condition上等待
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
//相当于obj.notify()/notifyAll()
condition.signal();
//condition.signalAll();
} finally {
lock.unlock();
}
}
Condition的常用方法:
await()
,awaitUninterruptibly(...)
,awaitNanos(...)
,awaitUnit(...)
,signal()
,signalAll()
Condition的获取只能通过Lock.newCondition()
获取,并且可以获取多个,多个等待队列,比如所有读线程在一个Condition上等待,所有写线程在另一个Condition上等待
5.Condition的实现
ConditionObject是AQS的内部类
5.1 等待队列
实际上是一个FIFO队列,复用了AQS同步队列的节点
一个Condition包含一个等待队列,Condition拥有首节点和尾结点,示意图:
Condition.await()
会新增节点,通过锁来保证更新节点的线程安全的(await之前已经获取到了锁)
AQS同步器拥有一个同步队列和多个等待队列:
每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用
5.2 等待
Condition.await()
会使线程进入等待队列并释放锁,同时线程变为等待状态
从队列角度看,相当于从同步队列首节点(获取了锁的节点)移动到了等待队列中:
ConditionObject.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()使线程等待
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);
}
5.3 通知
Condition.signal()
会唤醒在等待队列中等待时间最长的节点(首节点),并移动到同步队列中:
ConditionObject.signal()
方法代码如下:
public final void signal() {
//判断锁是否被当前线程持有
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//内部通过LockSupport.inpark(node.thread)唤醒线程
doSignal(first);
}
Condition的signalAll()
方法,相当于对等待队列中的每个节点均执行一次signal()
方法,唤醒所有该等待队列上的线程