文章目录
观看《Java并发编程的艺术》所做笔记
Java中的锁
Lock接口
队列同步器
AbstractQueuedSynchronizer
是构建锁和其他同步组件的基础框架
使用int类型表示同步状态,内置FIFO的双向队列(同步队列)来完成获取资源的排队工作
使用继承的方式使用队列同步器,子类实现同步器的抽象方法,管理同步状态
同步器提供getState(),setState(int newState),compareAndSetState(int expect,int update)
来实现同步状态的获得与修改
AQS提供的模板方法与可重写方法
-
访问修改同步状态
-
protected int getState()
: 获得当前同步状态 -
protected void setState(int newState)
: 设置当前同步状态 -
protected boolean compareAndSetState(int expect,int update)
: CAS原子性设置当前同步状态
-
-
模板方法(AQS中实现,子类不能改变)
模板方法可分为三类:
-
独占式获取/释放同步状态独占式:某一时刻只能有一个线程获取到同步状态
void acquire(int)
:独占式获取同步状态(不响应中断)void acquireInterruptibly(int)
:独占式获取同步状态(响应中断,线程被中断会抛出中断异常)boolean tryAcquireNanos(int,long)
: 独占式获取同步状态(响应中断,超时还未获取到同步状态则返回false)boolean release(int)
独占式释放同步状态
-
共享式获取/释放同步状态共享式:某一时刻可以有多个线程获取到同步状态
void acquireShared(int)
:共享式获取同步状态(不响应中断)void acquireSharedInterruptibly(int)
:共享式获取同步状态(响应中断)boolean tryArquireSharedNanos(int,long)
: 共享式获取同步状态(响应中断,超时还未获取同步状态返回false)boolean releaseShared(int)
:共享式释放同步状态
-
查询同步队列中等待线程情况
Collection<Thread> getQueuedThreads()
:获得同步队列中的线程
以上都是部分模板方法
-
-
可重写方法
-
protected boolean tryAcquire(int arg)
: 独占式获取同步状态 -
protected boolean tryRelease(int arg)
:独占式释放同步状态 -
protected int tryAcquireShared(int arg)
:共享式获取同步状态 -
protected boolean tryReleaseShared(int arg)
:共享式释放同步状态 -
protected boolean isHeldExclusively()
: 当前同步器是否在独占模式下被线程占用
-
AQS实现分析
AQS通过同步队列,独占式同步状态获取与释放,共享式同步状态获取与释放,超时获取同步状态等数据结构与模板方法完成线程同步
同步队列
AQS依赖内部的同步队列(双向队列,FIFO)来完成同步状态管理
同步队列中的节点用来保存获取同步状态失败的线程引用,等待状态,前驱后继节点等信息
节点属性
-
等待状态
int waitStatus
负值表示节点有效等待,正值表示节点被取消状态 说明 SIGNAL 值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,那么就会通知后继节点,让后继节点的线程能够运行 CONDITION 值为-2,节点在等待队列中,节点线程等待在Condition上,不过当其他的线程对Condition调用了signal()方法后,该节点就会从等待队列转移到同步队列中,然后开始尝试对同步状态的获取 PROPAGATE 值为-3,表示下一次的共享式同步状态获取将会无条件的被传播下去 CANCELLED 值为1,由于超时或中断,该节点被取消。 节点进入该状态将不再变化。特别是具有取消节点的线程永远不会再次阻塞 INITIAL 值为0,初始状态 -
前驱节点
Node prev
-
后继节点
Node next
-
等待队列中的后继节点
nextWaiter
-
获得同步状态的线程
Thread thread
同步队列基本结构
同步队列拥有首节点和尾节点(head tail)
遵循FIFO
-
线程获取同步状态失败时 设置尾节点(需要CAS保证安全)
- 将该线程的信息构造为一个节点并将其加入同步队列,并阻塞该线程
- 使用CAS操作设置尾节点 保证安全
-
同步状态释放时 设置首节点(不需要CAS操作)
-
首节点是获取同步状态成功的节点
-
首节点的线程在释放同步状态时,会唤醒后继节点,让后继节点尝试CAS获取同步状态,后继节点将会在获取同步状态成功时将自己设置为首节点
-
只有一个线程可以获得同步状态所以不需要CAS操作设置首节点
-
独占式同步状态的获取与释放
acquire(int)
- 不响应中断 : 调用
acquire(int)
尝试独占式获取同步状态,失败则构建节点加入同步队列,后续对该线程进行中断操作,线程不会从同步队列中移出
分析获取源码
acquire(int)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
-
acquire(int)
会去调用可重写的tryAcquire(int)
, 通过tryAcquire(int)
去尝试获取同步状态该方法需要保证线程安全的获取同步状态 -
如果获取同步状态失败,构建节点通过
addWaiter(Node.EXCLUSIVE)
方法加入同步队列队尾 Node.EXCLUSIVE:表示这是独占式:同一时刻只能有一个线程获得同步状态 -
通过
acquireQueued()
方法,让该节点以失败重试(死循环)的方式获取同步状态 -
如果获取不到同步状态则阻塞该节点中的线程 唤醒被阻塞的线程的方式: 1.前驱节点出队 2.阻塞线程被中断
注意: 执行tryAcquire()先尝试获取同步状态,这时候同步队列可能已经有节点在排队了(不公平锁的特性),获取失败才封装成节点加入同步队列,在同步队列中获取同步状态是公平的(如果要实现公平锁可以加个条件:先判断同步队列中有无前驱节点,有则乖乖去排队)
addWaiter(Node)
private Node addWaiter(Node mode) {
//构建节点 mode为独占式
Node node = new Node(Thread.currentThread(), mode);
//准备将旧的尾节点设置为前驱节点
Node pred = tail;
//如果旧的尾节点不为空
if (pred != null) {
//将旧的尾节点设置为当前节点的前驱节点
node.prev = pred;
//使用CAS操作尝试将当前节点设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//旧的尾节点为空会直接来执行enq 或者上面的CAS操作设置失败进入enq进行失败重试+CAS
enq(node);
return node;
}
enq(Node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//没有尾节点 需要初始化
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//失败重试 的 CAS操作设置尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(Node,int)
final boolean acquireQueued(final Node node, int arg) {
//是否失败(获取到同步状态就没失败)
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//1.p == head: 上一个节点是否是首节点
//2.tryAcquire(arg) : 尝试获取同步状态
if (p == head && tryAcquire(arg)) {
//设置当前节点为首节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire:失败获取后停放(设置一下状态,真正的停放在后面这个方法)
//parkAndCheckInterrupt:停放并检查是否中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//失败要取消节点在队列中的等待
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前驱节点状态是SIGNAL 说明前驱释放同步状态回来唤醒 很安心直接返回
return true;
if (ws > 0) {
//如果前驱状态大于0 说明被取消了,就一直往前找,找到没被取消的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//排在没被取消的节点后面
pred.next = node;
} else {
//前驱没被取消,而且状态不是SIGNAL CAS将状态更新为SIGNAL,释放同步状态要来唤醒我
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
//线程进入等待状态...
LockSupport.park(this);
//检查是否中断 (会清除中断标记位)
return Thread.interrupted();
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
查看该方法可知道: 节点进入同步队列后,一直自旋(CAS+失败重试),直到满足条件前驱节点是头节点,并且成功获取到同步状态才可以离开同步队列
自旋的CAS失败会被park,进入等待状态,等待前驱唤醒
为什么前驱节点是头节点才可以尝试获取同步状态?
- 维护队列的FIFO
- 头节点是成功获取到同步状态的节点,头节点释放同步状态后,将会唤醒它的后继节点 后继节点被唤醒后会检查自己的前驱节点是否为头节点
执行流程图
分析释放源码
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)
释放同步状态, 释放同步状态后会唤醒其后继节点进而使得后继节点重新尝试获取同步状态
总结
-
队列同步器维护了一个同步队列
-
同步器调用
tryAcquire(int)
尝试获取同步状态 -
当线程获取同步状态失败时
3.1 构建代表这个线程的节点,CAS操作把这个节点设置为队列的尾节点,在队列中进行自旋 (期间CAS失败会被park,进入等待状态)节点出队的前提: 该节点的前驱节点为首节点且该节点成功获取到同步状态(此时该节点肯定是首节点)
3.2 若前驱节点不是首节点进入等待队列(等待其他线程对该线程中断操作或前驱节点唤醒该线程)
3.3 若前驱节点为首节点尝试获取同步状态,成功则设置自己为首节点,失败则进入等待队列 -
释放同步状态:时
4.1 同步器调用
tryRelease(int)
释放同步状态4.2 唤醒首节点的后继节点,后继节点尝试获取同步状态
4.3 如果后继节点成功获得同步状态则原来的首节点出队,这个后继节点成为新的首节点
共享式同步状态的获取与释放
共享式与独占式的主要区别: 共享式能在同一时刻多个线程同时获取到同步状态,而独占式同一时刻只能一个线程获取到同步状态
就像程序读写文件一样,同一时刻允许多个线程一起读操作,不允许一起写操作
共享式访问资源时: 其他共享式访问可以被允许
独占式访问资源时: 其他(无论共享/独占式)访问都会被阻塞
分析获取源码
acquireShared(int)
public final void acquireShared(int arg) {
//tryAcquireShared(arg)尝试获取同步状态
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
doAcquireShared(int)
private void doAcquireShared(int arg) {
//构建 共享式的节点 CAS+失败重试保证线程安全的将该节点设置为尾节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// p 为 当前节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是首节点 再尝试获取同步状态
if (p == head) {
int r = tryAcquireShared(arg);
//如果成功则设置当前节点为首节点,断开旧的首节点方便GC,唤醒后继节点,退出
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);
}
}
- 调用可重写的
tryAcquireShared(int)
- 如果获取成功
acquireShared(int)返回值>=0
则退出 - 如果获取失败
acquireShared(int)返回值<0
则调用doAcquireShared(int)
- 构建共享式节点,调用
addWaiter(Node)
:CAS+失败重试的方式安全的将该节点设置为首节点 - 在
doAcquireShared(int)
中开始自旋(期间获取同步状态失败,会被park,进入等待状态),直到前驱节点为首节点并且该线程成功获取到同步状态(tryAcquireShared(int)返回值>=0
) 则可退出自旋
分析释放源码
releaseShared(int)
public final boolean releaseShared(int arg) {
//tryReleaseShared(arg)尝试释放同步状态
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(int)
尝试释放同步状态- 如果失败则返回false
- 如果成功则执行
doReleaseShared()
:做一些释放完的后续操作: 唤醒后继节点等…
因为共享式允许同一时刻多个线程获取同步状态,所以释放同步状态的操作可能来自于多个线程,因此也需要保证线程安全 由重写的tryReleaseShared(int)
保证多个线程安全释放同步状态
总结
-
同步器调用
tryAcquireShared(int)
尝试获取同步状态 -
获取同步状态失败时,执行
doAcquireShared(int)
(资源允许最大线程数已满) 2.1 执行
addWaiter(Node)
:构建共享式节点,CAS+失败重试 设置为队尾节点 2.2 开始自旋(期间获取同步状态失败,会被park,进入等待状态),直到前驱节点为首节点,该节点代表的线程成功获取到同步状态
-
释放同步状态:
3.1 同步器调用
tryReleaseShared(int)
CAS+失败重试保证线程安全 3.2 唤醒后继节点
与独占式过程大同小异
独占式超时获取同步状态
超时获取: 在指定时间内能够获取同步状态则返回true,不能则返回false
特点: 响应中断 + 超时获取
分析获取源码
doArquireNanos(int,long)
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果指定的时间是负数直接返回false
if (nanosTimeout <= 0L)
return false;
//deadline最后期限=当前时间+指定时间(纳秒)
final long deadline = System.nanoTime() + nanosTimeout;
//构建独占式节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//p为当前节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是首节点 并且 成功获取到同步状态
//则把当前节点设置为首节点,断开前驱节点方便GC,返回true
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//nanosTimeout超时纳秒=deadline最后期限-当前时间(纳秒)
//nanosTimeout<0说明已经超时
//nanosTimeout>0说明未超时还可以再等待nanosTimeout纳秒
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//spinForTimeoutThreshold = 1000L;
//未超时 继续等待,如果等待时间<1000ns 则自旋
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//如果线程被中断则抛出异常(响应中断)
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与独占式获取同步状态的区别
独占式超时获取同步状态与独占式获取同步状态区别
-
获取成功时相同 : (自旋中,当节点的前驱节点为首节点并且成功获取到同步状态就退出)
-
获取失败时不同:
- 如果没有超时(nanosTimeout>0)且 nanosTimeout > spinForTimeoutThreshold(1000纳秒) 则会让线程等待nanosTimeout纳秒,从
LockSupport.parkNanos(this, nanosTimeout)
中返回出来 - 当nanosTimeout < 1000纳秒时 不会让线程超时等待 而是快速自旋 原因是非常短的超时无法做到特别精确
- 如果没有超时(nanosTimeout>0)且 nanosTimeout > spinForTimeoutThreshold(1000纳秒) 则会让线程等待nanosTimeout纳秒,从
流程图
独占式超时获取同步状态流程图
自定义同步组件
需求: 同一时刻,允许至多3个线程同时访问,超过3个线程将被阻塞
- 该组件是共享式
- 资源数是3 所以同步状态可以初始化为3 ,同步状态范围0~3(3:还可以有3个线程获取同步状态,0:已经有3个线程获取了同步状态,此时会阻塞其他线程)
- 同步状态变更时需要保证线程安全 需要使用
compareAndSet(int expect,int update)
保证原子性
代码
/**
* @author Tc.l
* @Date 2020/12/7
* @Description: 自定义同步组件实现Lock接口
*/
public class MySynchronizerComponent implements Lock {
//初始化同步状态数为3
private Sync sync = new Sync(3);
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public void lock() {
sync.acquireShared(1);
}
//自定义同步器继承AQS 自定义同步器一般为同步组件的内部类
private static final class Sync extends AbstractQueuedSynchronizer {
public Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must more than zero");
}
//初始化设置同步资源数
setState(count);
}
/**
* 共享式获取同步状态
*
* @param arg
* @return 返回负数说明获取同步状态失败, 返回正数说明获取同步状态成功
*/
@Override
protected int tryAcquireShared(int arg) {
for (; ; ) {
//获得当前同步状态数
int currentState = getState();
//新的同步状态数
int newState = currentState - arg;
//如果新的同步状态数小于0就不会更新同步状态,只有新的同步状态数大于等于0时才会CAS更新同步状态
if (newState < 0 || compareAndSetState(currentState, newState)) {
return newState;
}
}
}
/**
* 共享式释放同步状态
*
* @param arg
* @return
*/
@Override
protected boolean tryReleaseShared(int arg) {
for (; ; ) {
//获得当前同步状态
int currentState = getState();
//新的同步状态
int newState = currentState + arg;
//如果更新同步状态成功则退出
if (compareAndSetState(currentState, newState)) {
return true;
}
}
}
}
}
实现Lock接口,只使用了lock(),unlock()
方法
测试
public static void main(String[] args) {
final Lock lock = new MySynchronizerComponent();
class Work extends Thread{
@Override
public void run() {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"拿到了锁===============");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public Work(String name) {
super(name);
}
}
for (int i = 0; i < 10; i++) {
Work work = new Work("工作线程" + i);
work.start();
}
for (int i = 0; i < 10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
工作线程1拿到了锁===============
工作线程2拿到了锁===============
工作线程0拿到了锁===============
工作线程9拿到了锁===============
工作线程3拿到了锁===============
工作线程4拿到了锁===============
工作线程8拿到了锁===============
工作线程7拿到了锁===============
工作线程6拿到了锁===============
工作线程5拿到了锁===============
*/
自定义的同步组件完成了最多3条线程访问的需求
总结
- 同步组件的自定义同步器一般是内部类且需要继承
AbstractQueuedSynchronizer
,在这里为了完成需求,我实现了tryAcquireShared(int),tryReleaseShared(int)
保证获取,释放同步状态时,更新同步状态的原子性 - 同步组件实现了Lock接口,这里只使用了
lock(),unlock()
面向调用者 ,用户使用的时候只需要调用接口方法lock(),unlock()
- 自定义同步器面向线程访问和同步状态控制 用户调用
lock()
时,同步器去执行acquireShared(int)
从而去执行重写的tryAcquireShared(int)
,线程安全的更新同步状态
重入锁
ReentrantLock 重入锁:支持重进入,该锁支持一个线程对资源的反复加锁
在不可重入锁中,调用tryAcquire(int)
第一次获得锁后返回true,同一线程再次对该资源进行加锁,再次调用tryAcquire(int)
此时会返回false,阻塞自己
可重入锁则不会发生这样阻塞自己的情况,一般它会去判断再次加锁时的线程是否为已经获取到锁的线程
同步关键字synchronized
是隐式的重入锁
同步组件ReentrantLock通过构造器可以指定为非公平锁(默认)或公平锁
自定义同步器Sync继承AQS
公平锁的同步器(FairSync)和非公平锁的同步器(NonFairSync)则是继承Sync
实现重进入
重进入: 任意线程获取锁后再次获取该锁不会发生阻塞
要想实现重进入,需要解决2个问题:
- 再次获取锁时,该如何处理?
- 锁最终释放在什么时候?
查看ReentrantLock中Sync的nonfairTryAcquire(int) 非公平锁获取同步状态
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果同步状态为0 说明没有线程获取
if (c == 0) {
//如果尝试CAS更新同步状态成功,设置当前线程为拥有独占访问权的线程,返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果同步状态不为0 说明已经有线程独占了该同步状态
//重入锁的关键: 判断当前线程是否为独占拥有访问权的线程
//不是则返回false 是则同步状态自增(因为只有一个线程拥有访问权所以不用CAS更新同步状态)
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;
}
- 同步状态为0时,说明没有线程获取同步状态,此时CAS更新同步状态成功,设置当前线程为拥有该锁访问权的线程,返回true 获取同步状态成功 (这是第一次加锁时的逻辑)
- 同步状态不为0时,说明有线程获取了同步状态,此时判断当前线程是否为拥有该锁访问权的线程(如果这个线程第一次加锁时已经获取到了,那么就会设置它自己是拥有该锁访问权的线程)
- 如果是则同步状态自增更新,返回true 获取同步状态成功(步骤1加锁后同步状态为1,此时如果是同个线程的第二次加锁则同步状态变为2)
- 如果不是则返回false 获取同步状态失败
重入锁的关键: 当同步状态不为0时,如果当前线程是拿到该锁的线程则同步状态+1,不会阻塞自己
ReentantLock中Sync的tryRelease(int)
protected final boolean tryRelease(int releases) {
//同步状态自减
int c = getState() - releases;
//如果当前线程不是获取锁的线程抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果同步状态自减后c为0,则设置独占访问权的线程为空,设置同步状态,返回true,说明此时的锁被完全释放
//如果同步状态自减后c不为0,则说明此次解锁后还有锁,并没有最终释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
重入锁允许线程获取锁后再次加锁,所以释放锁时也要释放相同次数的加锁才会最终释放锁 同个线程对同把锁加锁n次就要解锁n次
只有最终释放了锁,tryRelease(int)
才会返回true
公平锁与非公平锁的区别
上面nonfairTryAcquire(int)
是非公平锁的获取同步状态方法
查看ReentrantLock中FairSycn的tryAcquire(int)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors():当前节点在同步队列中是否有前驱节点
//有 则说明该节点不是首节点,返回true 这里!取反则变为false
//没有 则说明该节点是首节点,返回false 取反变为true 尝试更新同步状态
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;
}
公平锁对于重进入的逻辑处理与非公平锁类似,不同的则是只有首节点才可以获取同步状态
说明公平锁是FIFO的,需要排队的
总结
- 公平锁: 按照FIFO原则,会大量的切换线程,导致效率低
- 非公平锁: 极少的线程切换保证吞吐量高,可能出现线程饥饿(多线程抢占锁时,可能导致个别线程一直无法抢占到锁而导致它"饥饿")
读写锁
读写锁 ReenReadWriteLock:维护了一个读锁,一个写锁,写锁是排他锁(独占锁),读锁是共享锁
读写锁允许多个读线程访问,但写线程获取到写锁时,会阻塞其他的读写线程
在JDK5前 没有读写锁时: 当写线程拿到锁开始写操作时,所有的读写线程都进入等待,直到写操作完成,释放了锁,采取唤醒其他线程
有读写锁时: 当写线程拿到写锁开始写操作时,阻塞其他读写线程,直到写操作完成,释放了写锁,其他读写线程继续执行
读写锁在获取到写锁时阻塞其他读写线程,是为了保证读操作能够读取到正确的数据,不出现脏读
在读多于写的情况下,读写锁的并发性比排它锁要好
ReentrantReadWriteLock读写锁特性
- 支持选择公平锁与非公平锁(默认)
- 支持重进入
- 支持锁降级(获取写锁->获取读锁->释放写锁,为了保证数据可见性,不出现脏读)
- 不支持锁升级(获取读锁->获取写锁->释放读锁,这样不保证数据可见性,获取写锁后更新数据,获取读锁的线程对更新的数据是不可见的)
读写锁接口ReadWriteLock
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock
实现了ReadWriteLock
中的获取读锁,写锁的方法
还提供了一些便于外界监控其内部工作状态的方法
方法名 | 作用 |
---|---|
int getReadHoldCount() | 返回当前线程获取读锁次数 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
boolean isWriteLocked() | 当前写锁是否被获取 |
int getReadLockCount() | 返回当前读锁被获取次数 |
使用读写锁
public class ReentrantReadWriteLockTest<T> {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
private ArrayList<T> list = new ArrayList();
public T get(int index){
readLock.lock();
try {
return list.get(index);
}finally {
readLock.unlock();
}
}
public boolean put(T t){
writeLock.lock();
try {
return list.add(t);
}finally{
writeLock.unlock();
}
}
public T set(int index, T t){
writeLock.lock();
try {
return list.set(index,t);
}finally{
writeLock.unlock();
}
}
}
多线程中读操作(get())时不会发生阻塞,一旦有线程要完成写操作,拿到写锁才会阻塞其他线程
如果是排它锁则读的时候也会发生阻塞,在读操作多于写操作的场景,读写锁比排它锁的并发性能要好
读写锁的实现
读写锁设计
ReentrantReadWriteLock读写锁 同步组件也是依赖自定义同步器实现同步功能 读写状态就是同步状态
同步状态表示锁被线程获取的次数
读写锁将32位的int类型同步状态分为高16位的读状态和低16位的写状态
当有线程获取到写锁时,写状态+1,同步状态值+1,当有线程获取到读锁时,读状态+1,同步状态值+(1<<16)
写锁的获取与释放
查看ReentrantReadWriteLock的Sync的tryAcquire(int)
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//得到同步状态c
int c = getState();
//得到写状态
int w = exclusiveCount(c);
if (c != 0) {
//同步状态c不为0,写状态w为0说明读状态不为0,读锁已经被获取
//存在读锁 或者 当前线程不是获取写锁的线程 返回false获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//如果原来的写状态+这次重入的写状态 超过了 同步状态的0~15位 则抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//设置同步状态 因为写状态在低16位所以不用左移
setState(c + acquires);
return true;
}
//同步状态为0时 写应该阻塞或CAS更新同步状态失败则返回false 获取失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//写不应该阻塞或CAS更新成功则 设置当前线程为获得独占锁(写锁)的线程
setExclusiveOwnerThread(current);
return true;
}
查看源码可以知道获取写锁的前提要求:
- 如果同步状态不为0,不能有读锁被获取,且当前线程是获取独占锁(写锁)的线程 支持重入
- 如果同步状态为0,说明读写锁都未获取,CSA更新同步状态成功则设置获取独占锁的线程为当前线程,表示获取写锁成功
- 获取写锁时,要求读锁不能被获取
- 如果线程A获取读锁进行读操作,线程B获取写锁进行写操作,可能线程A读到的是线程B修改前的数据了,就出现了脏读
- 保证了数据可见性,防止脏读
写锁一旦被获取,其他读写线程被阻塞
读锁的获取与释放
查看ReentrantReadWriteLock的Sync的tryAcquireShared(int)
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//获得同步状态c
int c = getState();
//如果获得了写锁 并且 获得独占锁的不是当前线程则返回-1 获取失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获得写状态r
int r = sharedCount(c);
//写不应该阻塞 并且 写状态小于最大锁数 并且CAS更新同步状态成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
// SHARED_UNIT = (1 << SHARED_SHIFT); SHARED_SHIFT=16
compareAndSetState(c, c + SHARED_UNIT)) {
//此时同步状态已经被更新,但是r读状态还是更新前的旧状态
//旧的读状态r==0说明这是第一个获取读锁的线程
if (r == 0) {
//firstReader,firstReaderHoldCount 都是线程局部变量 是为getReadHoldCount()等方法记录的数据
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 1;
}
return fullTryAcquireShared(current);
}
因为getReadHoldCount()
等方法获取读锁变得复杂
获取读锁的核心:
- 如果其他线程获取了写锁,则获取失败,进入等待状态
- 如果获取写锁的是当前线程或者写锁未被获取,则使用CAS更新同步状态,成功获取读锁
锁降级
锁降级指的是:
- 当前线程先获取写锁
- 准备数据
- 当前线程再获取读锁
- 当前线程释放写锁
- 使用数据
锁降级的步骤3是必要的,如果没有步骤3再步骤4释放写锁时,有可能其他线程获得写锁修改数据,就会出现步骤5使用数据时的错误
为了数据可见性,防止脏读,步骤3的获取读锁是必要的
LockSupport工具
LockSupport工具提供了一组公共的静态方法,来阻塞线程(线程进入WAITING或TIMED_WAITING)和唤醒线程
LockSupport方法说明
一系列park开头的方法来让线程进入等待状态
unpark() 唤醒线程
LockSupport工具的使用
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
LockSupport.park();
});
Thread thread2 = new Thread(() -> {
LockSupport.parkNanos(100000000);
});
thread1.start();
thread2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread1.getState());
System.out.println(thread2.getState());
LockSupport.unpark(thread1);
}
/*
WAITING
TIMED_WAITING
*/
dump时提供锁对象信息
public static void main(String[] args) {
LockSupportTest lockSupportTest = new LockSupportTest();
Thread thread1 = new Thread(() -> {
LockSupport.park();
},"线程A");
Thread thread2 = new Thread(() -> {
LockSupport.park(lockSupportTest);
},"线程B");
thread1.start();
thread2.start();
}
Java 6为解决dump无法提供锁对象信息,推出的park(Object blocker) parkNanos(Object blocker, long nanos) parkUntil(Object blocker, long deadline)
来代替park() parkNanos(long nanos) parkUntil(long deadline)
Condition接口
每个对象中都有一组监视器方法wait(),notify()
等, 这些等待与唤醒的监视器方法 + synchronized 就可以实现等待/通知模式
Condition接口也提供了等待与唤醒的监视器方法 , 这些方法 用来与 Lock搭配使用,也可以实现等待/通知模式
Condition提供的方法
Condition提供一系列的await方法来让线程进入等待,signal,siganlAll方法来唤醒等待中的线程
Condition的使用
使用前提: 1. 线程拿到锁 2. Lock.newCondition
获取condition对象
public class ConditionTest {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionAwait() {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
System.out.println("线程被中断");
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void conditionSignal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
一般condition作为对象的字段来使用, 当线程A调用condition的await()时,线程A释放锁进入等待模式,等到其他线程调用这个condition对象的signal()时,就会唤醒线程A,此时线程A需要拿到锁才能返回到之前调用await()的位置
Condition实现分析
Condition的实现由AbstractQueuedSynchronizer的内部类ConditionObject来完成
因为使用ConditionObject前需要获取锁,所以ConditionObject是AbstractQueuedsynchronizer的内部类
等待队列
每个ConditionObject都包含着一个等待队列,等待队列是实现等待/通知的关键
ConditionObject的等待队列 与 AQS中的同步队列类似,等待队列中的节点也是复用AQS中的内部类Node
在等待队列中的节点(等待者),等待队列中还有字段记录首节点和尾节点(firstWaiter,lastWaiter
)
在Object监视器模型上,拥有一个等待队列和同步队列
在AQS同步器监视器模型上,拥有多个等待队列和一个同步队列 可以创建多个ConditionObject
等待
查看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工具类进入等待模式,等待唤醒
LockSupport.park(this);
//检查 该线程是否被中断,被中断了更新完中断状态值后退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//自旋尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//中断状态值不为0说明发生中断,抛出异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在 addConditionWaiter()
中不会以CAS操作去设置等待队列的尾节点,因为调用await的前提是拿到锁 通过锁来保证设置尾节点的线程安全
await流程
当线程获取锁后,调用condition对象的await()方法,那么该线程会释放锁,在等待队列中构建节点,并加入等待队列,进入等待状态
如果从队列的角度来看: 获得锁的线程一定是同步队列的首节点,它调用await方法,释放锁 并不是直接从同步队列移动到等待队列中,而是重新构建节点加入到等待队列队尾
通知
调用该condition对象的signal方法时会去唤醒等待时间最长的节点(也就是等待队列中的首节点)
查看ConditionObject的signal
public final void signal() {
//如果当前线程未获得锁抛出异常
if (!isHeldExclusively())
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;
//transferForSignal(first):同步队列队尾加入这个等待队列中原来的首节点,并唤醒线程
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//使用enq方法CAS设置同步队列尾节点
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//LockSupport工具类唤醒该线程
LockSupport.unpark(node.thread);
return true;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 前提: 当前线程必须获取锁,否则抛出异常
- 将等待队列中的首节点移出,通过enq方法将该节点CSA设置为同步队列的尾节点
- 使用LockSupport工具类唤醒该节点的线程
- 因为此时节点已经在同步队列,所以被唤醒后的线程会从await()方法中的while (!isOnSyncQueue(node)) 循环中退出
- 进而调用同步器的acquireQueued(node, savedState)加入到获取同步状态的竞争中
signalAll()
则是对等待队列中的所有节点进行signal()
方法