线程进阶
1、原子操作CAS
什么是原子操作
原子操作是指一个或者多个不可再分割的操作。这些操作的执行顺序不能被打乱,这些步骤也不可以被切割而只执行其中的一部分(不可中断性)
在Java中,我们可以通过同步锁或者CAS操作来实现原子操作.
1.1 CAS操作
CAS是Compare and swap的简称,这个操作是硬件级别的操作,在硬件层面保证了操作的原子性。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
2、显式锁Lock
synchronized 是java底层层面的一个比较完备的锁机制
lock 是java语言层面上的锁
语言层面上锁:
Lock: 有一个实现类可重入锁ReentrantLock
ReadWriteLock:读写锁 ReentrantReadWriteLock
Lock接口和synchronized的比较
synchronized 代码简洁,
Lock:获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用
2.1、可重入锁ReentrantLock
涉及到两个概念:公平锁和不公平锁
区分:在同一个时刻,多个线程同时请求一把锁,如果是按照谁先到达请求谁获取锁 就是公平锁
非公平的效率一般来讲更高
ReentrantLock和Syn关键字,都是排他锁,
ReentrantLock有两个构造方法,无参构造方法调用的是NonFairSync这个AQS子孙类来赋值给sync属性。调用无参构造方法的ReentrantLock是非公平锁,也是我们推荐使用的。我们可以通过调用ReentrantLock的有参构造方法来决定使用公平锁还是非公平锁。
代码实现可充入锁:
public class LockDemo {
private Lock lock = new ReentrantLock();
private int count;
public void increament() {
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
public synchronized void incr2() {
count++;
incr2();
}
public synchronized void test3() {
incr2();
}
}
2.1.1 实现重进入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
2.1.2 ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取同步状态的代码如代码所示
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
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;
}
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回
true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,该方法的代码如代码所示。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
2.1.3 公平与非公平获取锁的区别
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
ReentrantLock的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了
hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
2.2、 读写锁ReentrantReadWriteLock
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
代码实现读写锁:
定义一个实体:
/**
*
*类说明:商品的实体类
*/
public class GoodsInfo {
private final String name;
private double totalMoney;//总销售额
private int storeNumber;//库存数
public GoodsInfo(String name, int totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
public double getTotalMoney() {
return totalMoney;
}
public int getStoreNumber() {
return storeNumber;
}
public void changeNumber(int sellNumber){
this.totalMoney += sellNumber*25;
this.storeNumber -= sellNumber;
}
}
定义一个service:
/**
*
*类说明:商品的服务的接口
*/
public interface GoodsService {
public GoodsInfo getNum();//获得商品的信息
public void setNum(int number);//设置商品的数量
}
可重入锁代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.xiangxue.tools.SleepTools;
/**
*
*类说明:
*/
public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//读锁
private final Lock setLock = lock.writeLock();//写锁
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public GoodsInfo getNum() {
getLock.lock();
try {
SleepTools.ms(5);
return this.goodsInfo;
}finally {
getLock.unlock();
}
}
@Override
public void setNum(int number) {
setLock.lock();
try {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}finally {
setLock.unlock();
}
}
}
测试:
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import com.xiangxue.tools.SleepTools;
/**
*
*类说明:对商品进行业务的应用
*/
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
//static CountDownLatch latch= new CountDownLatch(1);
//读操作
private static class GetThread implements Runnable{
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
// try {
// latch.await();//让读写线程同时运行
// } catch (InterruptedException e) {
// }
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
+(System.currentTimeMillis()-start)+"ms");
}
}
//写操做
private static class SetThread implements Runnable{
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
// try {
// latch.await();//让读写线程同时运行
// } catch (InterruptedException e) {
// }
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0;i<10;i++){//操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000);
GoodsService goodsService = new UseRwLock(goodsInfo);/*new UseSyn(goodsInfo);*/
for(int i = 0;i<minthreadCount;i++){
Thread setT = new Thread(new SetThread(goodsService));
for(int j=0;j<readWriteRatio;j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
SleepTools.ms(100);
setT.start();
}
//latch.countDown();
}
}
读写锁:同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有的读和写都被阻塞,最适宜与读多写少的情况
2.2.1 读写锁的实现分析
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,
划分方式如图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3P8BbDTN-1594054638132)(C:\Users\嘉松\Desktop\线程笔记\image-20200707004005731.png)]
假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移
16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
2.3、 Condition接口
Condition实现wait/notify功能
await()相当于wait()方法
await(long time, TimeUnit unit)相当于wait(long)方法
signal()相当于notify()方法
signalAll()相当于notifyAll()方法
await()和signal()同样需要在lock.lock()之后调用,否则会报监视器出错。
用lock和Condition实现一个超时等待代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
*
*类说明:
*/
public class ExpressCond {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
private Lock lock = new ReentrantLock();
private Condition keCond = lock.newCondition();
private Condition siteCond = lock.newCondition();
public ExpressCond() {
}
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public void changeKm(){
lock.lock();
try {
this.km = 101;
keCond.signalAll();
}finally {
lock.unlock();
}
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public void changeSite(){
lock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
}finally {
lock.unlock();
}
}
/*当快递的里程数大于100时更新数据库*/
public void waitKm(){
lock.lock();
try {
while(this.km<=100) {
try {
keCond.await();
System.out.println("check km thread["+Thread.currentThread().getId()
+"] is be notifed.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the Km is "+this.km+",I will change db");
}
/*当快递到达目的地时通知用户*/
public void waitSite(){
lock.lock();
try {
while(CITY.equals(this.site)) {
try {
siteCond.await();
System.out.println("check site thread["+Thread.currentThread().getId()
+"] is be notifed.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the site is "+this.site+",I will call user");
}
}
测试代码:
/**
*
*类说明:测试Lock和Condition实现等待通知
*/
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递里程变化
}
}
2.3.1 Object的监视器方法与Condition接口的对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YEpQ6YA-1594054638135)(C:\Users\嘉松\Desktop\线程笔记\image-20200707004246877.png)]
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。
Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
2.3.2 Condition的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
2.3.2.1 等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,
如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态
节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node
当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsR4zJIw-1594054638137)(C:\Users\嘉松\Desktop\线程笔记\image-20200707004608748.png)]
Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。
上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQa2Oc9s-1594054638139)(C:\Users\嘉松\Desktop\线程笔记\image-20200707004719805.png)]
Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。
2.3.2.2 等待
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
// 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(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();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
2.3.2.3 通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DvU3VX7Z-1594054638141)(C:\Users\嘉松\Desktop\线程笔记\image-20200707005148155.png)]
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。
接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
节点从等待队列移动到同步队列的过程如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r8Sf9JUW-1594054638143)(C:\Users\嘉松\Desktop\线程笔记\image-20200707005353818.png)]
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。 所以建议调用signa() 方法来唤醒等待的线程
2.4、synchronized与Lock的区别
1、我把两者的区别分类到了一个表中,方便大家对比:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定 | Lock有多个锁获取的方式,可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
3、LockSupport工具
作用:
- 阻塞一个线程 void park() 阻塞当前线程
- 唤醒一个线程 void unpark() 唤醒处于阻塞状态的线程 thread
- 构建同步组件的基础工具
4、AbstractQueuedSynchronizer深入分析
抽象的队列同步器
1、队列同步器AbstractQueuedSynchronizer,用来构建锁或者其他同步组件的基础框架,使用int成员变量表示同步状态,通过内置的FIFO(先进先出)队列来完成资源获取线程的排队工作。
2、同步器的主要方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。需要使用同步器提供的3个方法
- getState() 获取当前状态
- setState(int newState) 设置当前状态
- compareAndSetState(int expect, int update) 使用CAS设置当前状态,保证原子性。
他们能保证状态的改变是安全的。
3、子类推荐被定义为自定义组件的静态内部类,同步器没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方式来供自定义同步组件来使用。同步器既可以支持独占式的获取同步组件(ReentrantLock),也可以支持共享式地获取同步状态(ReentrantReadWriteLock)
4、同步器的设计是基于模版方法模式的。也就是说,使用者需要继承同步器并实现重写指定的方法,随后将同步器组合到自定义同步组件的实现中,并调用同步器提供的模版方法即可。
4.1 同步器方法说明
同步器可重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire( int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease( int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected boolean tryAcquireShared( int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared( int arg) | 共享式释放锁 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
模版方法描述
- 独占式获取与释放同步状态
accquire(int arg)
acquireInterruptibly(int arg)
tryAcquireNanos(int arg, long nanos)
release(int arg)
- 共享式获取与释放同步状态
acquireShared(int arg)
acquireSharedInterruptibly(int arg)
tryAcquireSharedNanos(int arg, long nanos)
releaseShared(int arg)
4.2 自定实现一个同步组件
public class Mutex implements Lock{
//state 表示获取到锁 state=1 获取到了锁,state=0,表示这个锁当前没有线程拿到
private static class Sync extends AbstractQueuedSynchronizer{
//是否占用
protected boolean isHeldExclusively() {
return getState()==1;
}
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)) { // CAS
setExclusiveOwnerThread(Thread.currentThread()); // 设置当前线程为独占式
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
if(getState()==0) {
throw new UnsupportedOperationException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sycn = new Sync();
@Override
public void lock() {
sycn.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sycn.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sycn.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sycn.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sycn.release(1);
}
@Override
public Condition newCondition() {
return sycn.newCondition();
}
}
4.3 同步队列
同步器依赖内部的同步队列FIFO 来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会将首节点的线程唤醒,使其再次尝试获取同步状态
同步队列中的节点(Node)代码如下
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
// 表示节点正在共享模式下等待的标记
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
// 独占节点
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus; // 等待状态
volatile Node prev; // 前一个阻塞的节点
volatile Node next; // 下一个阻塞节点
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread; // 当前阻塞的线程
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步队列的基本结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Gff99yx-1594054638144)(C:\Users\嘉松\Desktop\线程笔记\image-20200706220043176.png)]
节点加入到同步队列的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HboGuJrB-1594054638145)(C:\Users\嘉松\Desktop\线程笔记\image-20200706220146113.png)]
当一个线程成功的获取了同步状态(锁),其他线程将无法获取到同步状态,转而被构造成节点加入到同步队列当中,而加入这个队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法CompareAndSetTail(Node expect, Node update)。
首节点的变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9vbQ918p-1594054638147)(C:\Users\嘉松\Desktop\线程笔记\image-20200706220633786.png)]
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继结点,而后继结点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程完成的,由于只有一个线程能够成功获取到同步状态,因此设置头结点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
4.4 独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法代码如代码清单所示。
public final void acquire(int arg) {
if (!tryAcquire(arg) // 同步状态获取
&&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
*addWaiter(Node.EXCLUSIVE) 节点构造、加入同步队列
acquireQueued 在同步队列中自旋
*/
其主要逻辑是:
- 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)
- 通过addWaiter(Node node)方法将该节点加入到同步队列的尾部
- 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
4.4.1节点的构造以及加入同步队列,如代码清单所示。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
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;
}
}
}
}
通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)如代码清单所示。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,
这是为什么?原因有两个,如下。
第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会
唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
第二,维护同步队列的FIFO原则。该方法中,节点自旋获取同步状态的行为如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGxappmJ-1594054638148)(C:\Users\嘉松\Desktop\线程笔记\image-20200707000634445.png)]
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。
可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。
4.4.2独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vc3Cx43H-1594054638149)(C:\Users\嘉松\Desktop\线程笔记\image-20200707000919022.png)]
前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。该方法代码如代码所示。
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 arg)方法释放同步状态,然后唤醒头节点的后继节点。
4.5 共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别:同一时刻能否有多个线程同时获取到同步状态。
以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。
写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,
4.5.1 共享同步器的acquireShared和doAcquireShared方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
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);
}
}
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。
4.5.2 共享式获取释放同步状态
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
在释放同步状态之后,将会唤醒后续处于等待状态的节点。
对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
4.6 设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock。
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for (;;) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) { for (;;) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) { return true;
}
}
}
}
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
}