抽象队列同步器AQS之Lock详解
前言
从今天开始连续几篇进行AQS底层分析,看源码,本篇是开篇
Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕着共同的基础行为,
比如等待队列、条件队列、独占获取、共享获取等,
而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,
AQS定义了一套多线程访问共享资源 的同步器框架,是一个依赖状态(state)的同步器
ReentrantLock整体流程
//使用ReentrantLock进行同步 //false为非公平锁, true为公平锁
ReentrantLock lock = new ReentrantLock(false)
lock.lock() //加锁
业务逻辑
lock.unlock() //解锁
我们分析下这个lock的过程
lock.lock() //加锁
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
假设有T0,T1,T2三个线程,拿到锁的T0将会执行业务逻辑,而没拿到锁的T1,T2将在lock方法里,不可能下来,那怎么可以用自选的方式,让T1,T2一直在while循环里
lock.lock() //加锁
while(true){
if(cas加锁成功){//cas->比较与交换compare and swap,
break;跳出循环
}
}
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
假设有很多线程,都在while循环里去跑,会浪费CPU,所以可以
lock.lock() //加锁
while(true){
if(cas加锁成功){//cas->比较与交换compare and swap,
break;跳出循环
}
Thread.yeild()//让出CPU使用权
Thread.sleep(1);
}
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
但是如果T0在下面执行的业务逻辑时间很长,不可能让CPU一直处于礼让状态。所以可以睡眠一下,睡眠多久合适呢,需要判断睡眠多久合适。但是这个时间不好控制,所以就有了采取阻塞的方式。
lock.lock() //加锁
while(true){
if(cas加锁成功){//cas->比较与交换compare and swap,
break;跳出循环
}
//Thread.yeild()//让出CPU使用权
//Thread.sleep(1);
阻塞。
LockSupport.park();
}
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
采取阻塞,让线程停在这里下不来。但是不能一直阻塞在这里。当T0解锁了,需要唤醒
lock.lock() //加锁
while(true){
if(cas加锁成功){//cas->比较与交换compare and swap,
break;跳出循环
}
//Thread.yeild()//让出CPU使用权
//Thread.sleep(1);
阻塞。
LockSupport.park();
}
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
LockSupport.unpark(线程);
那么线程是哪里来的呢,比如用HashSet,LikedQueued进行存储,在T0解锁后就可以从队列或者set里拿出来。
lock.lock() //加锁
while(true){
if(cas加锁成功){//cas->比较与交换compare and swap,
break;跳出循环
}
//Thread.yeild()//让出CPU使用权
//Thread.sleep(1);
HashSet,LikedQueued(),
HashSet.add(Thread)
LikedQueued.put(Thread)
阻塞。
LockSupport.park();
}
T0获取锁
xxxx业务逻辑
xxxxx业务逻辑
lock.unlock() //解锁
Thread t= HashSet.get()
Thread t = LikedQueued.take();
LockSupport.unpark(t);
当T0完成后,唤醒T1,T1进行自旋,当加锁成功后跳出循环,执行业务逻辑。T2一样
Lock三大核心原理
自旋,LocksSuport, CAS(加锁,不管有多少线程来,永远要保证只有1个线程能加锁成功),
数据结构采用:queue队列,原因:要想实现公平和非公平,需要排队,FIFO。就保证了公平性。
CAS
假设主内存expect值是0,我们有俩个线程要修改这个值。
俩个线程都读一份,更新的值refresh,A是1,B是2,假设这俩个线程都要去修改主内存的值
假设是线程B先去修改主内存值,会拿线程B expect和主内存里的expect去做比对,
如果相等,就把refresh值写到主内存的expect里,不相等不能修改。
此时线程A比较发现不相等,不能修改,如果修改需要把主线程的值(此时变为1)
再次读取,下次修改的时候进行比较交换。
ReentrantLock底层源码分析
ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现 这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个 ReentrantLock同时具备公平与非公平特性。
先看下类图,选中AbstractQueuedSynchronizer,按住TRL+t,出现后全选回车
公平锁与非公平锁的代码区分
当前获取线程是谁
锁被谁记录,拿了多少次。当state为0表示当前锁没被任何人持有,可以进行加锁
Node里有个对线程引用的变量,基于这个引用去unpark唤醒
核心的属性都介绍了,以公平锁为例简单看下:
下图tryAcquire方法,尝试去获取锁,首先把当前线程引用拿出来,然后查看state
c==0不是一上来就可以去取,因为会有排队,所以需要!hasQueuedPredecessors(),怎么判断队列有没有线程,判断对头head和队尾tail是不是null。是就是空队列
队列没有等待的,然后CAS,将state值由0>1。然后
第2个判断,不存在并发
如果tryAcquire尝试获取锁失败,例如T2,T3则进行acquireQuened方法。
selfInterrupt
程序员自己定义的代码也要识别到中断信号,中断信号往外面的代码去传。这个在哪有用呢?
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}