Java多线程 同步队列详解(AQS)
文章目录
- Java多线程 同步队列详解(AQS)
- 1、AQS简介
- 1.1 什么是AQS
- 1.2 什么是CLH锁队列
- 2、ASQ 提供的各种锁实现流程
- 2.1 独占锁
- 2.2 共享锁
- 2.3 可重入锁
- 2.4 公平锁与非公平锁
- 2.5 读写锁
- 3、基于ReentrantLock实现的各种阻塞队列
- 3.1Java阻塞队列详解
1、AQS简介
1.1 什么是AQS
队列同步器(AQS)是用来构建锁或者其他同步组件的基础框架,使用一个int型变量代表同步状态,通过内置的队列来完成线程的排队工作。
根据其API,总结来说就是:
①子类通过继承AQS并实现其抽象方法来管理同步状态,对于同步状态的更改通过提供的getState()、setState(int state)、compareAndSetState(int expect, int update)来进行操作,因为使用CAS操作保证同步状态的改变是原子的。
②子类被推荐定义为自定义同步组件的静态内部类,同步器本身并没有实现任何的同步接口,仅仅是定义了若干状态获取和释放的方法来提供自定义同步组件的使用。
③同步器既可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态(ReentrantLock、ReentrantReadWriteLock、CountDownLatch等不同类型的同步组件)
ASQ定义了两种资源共享的方式:
(1)独占,只有一个线程能执行,如ReentrantLock;
(2)共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
关于同步器的几个重要方法 :
(1)sHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
(2)tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
(3)tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
(4)tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
(5)tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
1.2 什么是CLH锁队列
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
CLH队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点插入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。
结构如下:
2、ASQ 提供的各种锁实现流程
2.1 独占锁
1、获取锁的过程
(1)调用入口方法acquire(arg)
(2)调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
(3)将当前线程构造成一个Node节点,并利用CAS将其加入到同步队列到尾部,然后该节点对应到线程进入自旋状态
(4)自旋时,首先判断其前驱节点是否为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程的节点设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待被前驱节点唤醒
2、释放锁的过程
(1)调用入口方法release(arg)
(2)调用模版方法tryRelease(arg)释放同步状态
(3)获取当前节点的下一个节点
(4)利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第四步)
2.2 共享锁
1、获取锁过程
(1)调用acquireShared(arg)入口方法
(2)进入tryAcquireShared(arg)模版方法获取同步状态,如果返返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回
如果tryAcquireShared(arg)返回值<0,说明获取同步状态失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态
(3)自旋时,首先检查前驱节点是否为头节点 && tryAcquireShared()是否>=0(即成功获取同步状态)
(4)如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点
(5)如果否,则利用LockSupport.unpark(this)挂起当前线程,等待被前驱节点唤醒
2、释放锁过程
(1)调用releaseShared(arg)模版方法释放同步状态
(2)LockSupport.unpark(nextNode.thread)唤醒所有后继节点。
3、实例演示(信号量Semphroe ):
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
Semaphore实现的功能就像:银行办理业务,一共有5个窗口,但一共有10个客户,一次性最多有5个客户可以进行办理,其他的人必须等候,当5个客户中的任何一个离开后,在等待的客户中有一个人可以进行业务办理。
例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
public static void main(String[] args) {
//创建一个可根据需要创建新线程的线程池
ExecutorService service = Executors.newCachedThreadPool();
final Semaphore sp = new Semaphore(3);
//创建10个线程
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
try {
sp.acquire(); //获取灯,即许可权
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"进入,当前已有" + (3-sp.availablePermits()) + "个并发");
try {
Thread.sleep((long)(Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"即将离开");
sp.release(); // 释放一个许可,将其返回给信号量
//下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
System.out.println("线程" + Thread.currentThread().getName() +
"已离开,当前已有" + (3-sp.availablePermits()) + "个并发");
}
};
service.execute(runnable);
}
}
}
结果:
线程pool-1-thread-3进入,当前已有3个并发
线程pool-1-thread-2进入,当前已有3个并发
线程pool-1-thread-1进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-2已离开,当前已有2个并发
线程pool-1-thread-5进入,当前已有3个并发
线程pool-1-thread-1即将离开
线程pool-1-thread-1已离开,当前已有2个并发
线程pool-1-thread-4进入,当前已有3个并发
线程pool-1-thread-4即将离开
线程pool-1-thread-4已离开,当前已有2个并发
线程pool-1-thread-8进入,当前已有3个并发
线程pool-1-thread-3即将离开
线程pool-1-thread-7进入,当前已有3个并发
线程pool-1-thread-3已离开,当前已有3个并发
线程pool-1-thread-8即将离开
线程pool-1-thread-8已离开,当前已有2个并发
线程pool-1-thread-9进入,当前已有3个并发
线程pool-1-thread-7即将离开
线程pool-1-thread-7已离开,当前已有2个并发
线程pool-1-thread-6进入,当前已有3个并发
线程pool-1-thread-9即将离开
帮助理解:
(1)Semaphore sp = new Semaphore(3),创建了共享资源3个,相当于把state的值设置为3;
(2)sp.acquire(); 获取同步资源,若state > 0 , 获取资源成功,可执行,并将state - 1; 否则获取失败,并加入线程同步队列;
(3) sp.release(); 释放同步资源,并将state + 1,把线程同步中的队列都唤醒。
2.3 可重入锁
重入锁指的是当前线成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。
Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现。
1、ReentrantLock可重入锁实现的核心代码:
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//通过AQS获取同步状态
int c = getState();
//同步状态为0,说明临界区处于无锁状态,
if (c == 0) {
//修改同步状态,即加锁
if (compareAndSetState(0, acquires)) {
//将当前线程设置为锁的owner
setExclusiveOwnerThread(current);
return true;
}
}
//如果临界区处于锁定状态,且上次获取锁的线程为当前线程
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;
}
可见,可重锁的实现重点是判断当前正在执行的线程跟请求线程是否为同一个线程。
2.4 公平锁与非公平锁
1、公平锁
公平锁是指当多个线程尝试获取锁时,成功获取锁的顺序与请求获取锁的顺序相同;
对于公平锁,线程获取锁的过程可以用如下示意图表示
公平锁抢锁示意图
2、非公平锁
非公平锁是指当锁状态为可用时,不管在当前锁上是否有其他线程在等待,新近线程都有机会抢占锁
示意图
实例演示(ReentrantLock)可前往上一篇博客:
Java多线程 ReentrantLock与Condition
2.5 读写锁
Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
结构如下图:
1、写锁是一个独占锁,所以我们看一下ReentrantReadWriteLock中tryAcquire(arg)的实现:
获取:
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");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
(1)获取同步状态,并从中分离出低16为的写锁状态
(2)如果同步状态不为0,说明存在读锁或写锁
(3)如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
(4)如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
(5)如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
(6)将当前线程设置为写锁的获取线程
释放的过程与独占锁基本相同。
2、读锁是一个共享锁,获取读锁的步骤如下:
(1)获取当前同步状态
(2)计算高16为读锁状态+1后的值
(3)如果大于能够获取到的读锁的最大值,则抛出异常
(4)如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
(5)如果上述判断都通过,则利用CAS重新设置读锁的同步状态
读锁的获取步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。
在JDK1.6以后,读锁的实现比上述过程更加复杂,有兴趣的同学可以看一下最新的后去读锁的源码。
3、基于ReentrantLock实现的各种阻塞队列
3.1Java阻塞队列详解