什么是Reentrantlock
reentrantlock是lock接口的实现类,与synchronized的monitor中的count一样,reentrantlock也是一种可重入锁,支持线程对资源进行重复加锁,加锁多少次就要解锁锁少次,reentrantlock也支持公平锁和非公平锁,synchronized是隐式锁不能干预不能控制,reentrantlock实现的锁可以控制加锁和释放锁的过程。
package binfabianchen;
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
@Override
public void run() {
for(int j=0;j<10000000;j++){
lock.lock();
//支持重入锁
lock.lock();
try{
i++;
}finally{
//执行两次解锁
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock tl=new ReenterLock();
Thread t1=new Thread(tl);
Thread t2=new Thread(tl);
t1.start();
System.out.println(i);//刚开始执行在这里输出的是0
t1.join();//t1线程结束之后开始第二个线程
System.out.println(i);//输出的是10000
t2.start();// t2线程在地方开始
t2.join();
System.out.println(i);// 在这里输出20000
------------------------------------
如果是
t1.start()
System.out.println(i);//这里输出的值不一定是0
t2.start()
System.out.println(i);//这里输出的值不一定是0
t1.join()
System.out.println(i);//这里输出的值不一定是10000,因为start()已经启动过
这种情况下在这里输出的值就是
}
}
通过上面可以看出Lock lock =new ReentrantLock();实现了创建了lock实例,并且执行了两次加锁操作,之后在finally代码块中执行了两次释放锁的操作unlock()
Reentrantlock实现的方法
Reentrantlock实现了lock接口中的lock() unlock() trylock() trylock(time) lockinterruptibly() onCondition()方法之外还实现了其他的方法:
- int getHoldCount() 查询当前线程保持此锁的次数
- int getQueueLength() 查询正在等待此锁的线程的估计数
- protected Thread getOwner() 查询当前拥有此锁的线程 如果此锁不被线程拥有返回null值
- protected collection getQueuedThreads() 返回正在等待此锁的线程
- bolean hasQueuedThread(Thread thread)查询给定的线程是否正在等待获取此锁
- boolean hasQueuedThread() 查询是否有线程正在等待此锁
- boolean isHeldByCurrentThread() 查询当前线程是否保持拥有此锁
- boolean islocked() 查询此锁是否被线程持有
- boolean isfair() 查询此锁是否为公平锁
- protected Collection getWaitingThreads(Condition condition);返回包含可能正在等待与此锁相关给定条件的那些线程
- int getWaitQueueLength(Condition condition) 查询等待与此锁相关给定条件的线程估计数
- boolean hasWaiters(Condition condition)查询是否有些线程正在等待与此锁有关的给定条件
Reentrantlock类中的方法有很多都和线程相关,reentrantlock内部是通过AQS并发框架实现的,就是同步队列器AbstractQueuedSynchronized实现的
什么是AbstractQueuedSynchronizer
reentrantlock内部实际上是通过AbstractQueuedSynchronizer实现的,成为同步队列器是用来构建锁或者其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,state=0说明没有任何线程占有共享资源的锁,state=1说明有线程占有共享资源的锁,此时其他线程需要加入到同步队列中进行等待,AQS内部有一个由内部类Node构成的同步队列用于等待获取锁的线程进行排队,注意是同步队列说明abstractqueuesynchronizer以同步队列、status的配合管理锁的获取和释放,同时内部类ConditionObject构建一个等待队列,**当Conditon调用wait()方法后,**等待线程线程将会加入等待队列中,而当Condition调用signal()方法后,等待线程将从等待队列中移动到同步队列中进行锁竞争。
注意:涉及到了两种队列一种是同步队列,当线程请求锁而等待之后就加入同步队列中进行等待,另一种是等待队列,可以有多个等待队列,通过Condition调用await()方法释放锁后,将加入等待队列。
AbstractQueuedSynchronizer中包含一个同步队列头部,一个指向同步队列的尾部,还int类型的状态
public abstract calss AbstractQueuedSynchronizer extends AbstranctOwnableSynchronizer{
private transient volatile Node head;// 指向同步队列的队列头
private transient volatile Node tail;// 指向同步队列的对列尾
private volatile int state;// 同步状态,0未被占用1被占用
}
head和tail是AQS变量,内部类Node构成了同步队列,其中Node中的数据结构为pre waitStatus thread next 一个节点表示一个线程,指向前一个节点、指向后一个节点、当前的状态:收否被阻塞是否等待唤醒,是否已经取消等等,注意waitstatus的值表示表示当前线程的状态:是被阻塞了还是在等待唤醒还是已经取消了。
总结:abstractqueuesysnchronizer中的head和tail分别指向同步队列的头部和尾部,不存储信息,采用双向链表的表示结构可以方便节点增加和删除操作,abstractqueuesysnchronizer中的state变量表示同步的状态。lock实例调用lock()方法进行加锁的时候,如果此时lock实例内部state的值为0,则说明当前线程可以获取到锁,为什么说是state是lock实例中的内容呢,因为lock实例是通过reentrantlock创建的,而reentrantlock是有abstractqueuesynchronize实现的,因此我们通过abstractqueuesynchronzied进行判断是否是同步状态就是通过lock实例来实现的,同时将state设置为1,表示获取成功。如果state已经为1,就是说当前锁已经被其他线程持有,那么当前获取实例锁的线程将被封装为Node节点加入同步队列,Node节点(不光用在同步队列中也用在等待队列中)是对每一个访问同步代码的线程的封装,其中包含了需要同步的线程本身、线程的状态以及前继节点和后继节点,为了线程释放锁之后能够快速的唤醒下一个线程。
Node节点类
从node节点类中可以看出:node节点中有thread变量表示当前线程,pre 前置节点,next后置节点,还有等待队列中的后继节点,还有waitstatus状态变量,其中状态变量中包含cancelled已经结束状态、signal等待被唤醒状态 、condition条件状态、还有判断是独占模式还是共享模式的变量
static final class Node{
static final Node SHARED=new Node();//共享模式
static final Node EXCLUSIVE =null;// 独占模式
static final int CANCELLED=1;//结束状态
static final int SIGNAL=-1; //等待被唤醒状态
static fianl int CONDITION=-2;// 条件状态
static final int propagate=-3;// 在共享模式中使用表示获取的同步状态会被传播
volatile int waitStatus;// 等待状态变量
volatile Node prev; // 前置节点
volatile Node next;// 后置节点
volatile Thread thread;// 请求锁的线程
Node nextWaiter;等待队列中的后继节点,与condition相关
final boolean isShared(){// 判断是否是共享模式
return nextwaiter==SHARED;
}
final Node predecessor() throws NullPointerExxception{ // 获取前一个节点
Node p=prev;
if(p==null){
throw new nullPointerException():
}else{
return p;
}
}
}
什么是独占模式?什么是共享模式?
node节点中的中的SHARED和EXCLUSIVE常量分别表示共享模式和独占模式,共享模式表示的就是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于
AbstractQueuedSynchronier同步队列器的共享模式实现的,独占模式是同一时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReenTrantLock。
abstractqueuesynchronizer中的内部类node详细介绍
变量waitstatus表示当前被封装成Node节点的等待状态,共有4中取值CANCELLED,SIGNAL,CONDITION,PROPAGATE
0,1 -1 -2 -3
- CANCELLED:值为1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消该节点,其节点的waitStatus为CANCELLED,表示结束状态进入该状态的节点不会再发生变化
- SIGNAL:值为-1,表示等待被唤醒的节点,当其前继节点的线程释放了同步锁,或者被取消,将会通知该节点的线程执行获取锁的操作 。简单的说处于唤醒状态,只要前继节点释放锁就会通知为为SIGNAL状态的后继节点的线程执行
- CONDITION:值为-2,与Condition相关,表示节点处于等待队列中,节点的线程等待在Condition上,当其他线程调用了Condition的signal()方法之后,Condition状态的节点将从等待队列转移到同步队列中,等待获取同步锁。
- propagate:值为-3,共享模式相关,在共享模式中,该状态标识的线程处于可运行状态 0状态:值为0,代表初始化状态
prev、next节点变量
prev和next分别执行当前Node节点的前驱节点和后继节点
thread变量
thread变量存储请求锁的线程。
nextwaiter变量
nextWriter与condition相关**,代表等待队列中的后继节点。**
AQS作为基础组件,实现的锁存在两种模式:有共享模式Semaphore和独占模式ReentrantLock两种,无论是共享模式还是独占模式,内部都是基于同步队列器AQS实现的,也都维持了一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node节点并将线程当前的内容存储到Node节点中,然后加入同步队列中等待获取锁,无论是Semaphore还是ReentrantLock,其内部大多数方法是间接调用AQS完成的。这就是为什么abstractqueuesynchronizer被称为基础组件的原因。
AbstractQueueSynchronizer的整体结构
- AbstractQueuedSynchronizer:内部以内部类node构成的同步队列、等待队列和state变量管理线程的锁的获取和释放,其中acquire() release()方法提供了实现,tryAcquire()方法和tryRelease()方法没有提供默认实现,需要子类重写这两个方法,开发者可以自己定义获取锁和释放锁的实现方式。
- Sync继承与AbstractQueuedSynchronizer,实现了tryRelease())释放锁的操作,在内部声明了lock()方法没有实现
- NonfairSync:继承与abstractSync,在内部实现了lock和tryAcquire()方法,是公平锁的实现类
- fairSync:继承与abstractSync,在内部实现lock和tryAcquire()方法,是非公平锁的实现类
实际上abstractqueuesynchronizer中不仅有tryRelease、tryAcquire acquire release方法还有tryAcquireShared tryReleaseShared 两种方法,因为前面我们说过abstractQueueSynchronizer有两种模式一种模式是独占模式,一种模式是共享模式
为什么说Reentrantlock底层是abstractqueuesynchronizer实现的呢?
因为reentrantlock有三个内部类,这三个内部类支撑着reentrantlock实现的方法,这三个方法分别继承了abstractqueuesynchronizer,ReentrantLock中的内部类有Sync FairSync NofairSync,在创建的时候根据fair参数决定创建NonfairSync还是FairSync,其中lock方法是Sync提出的,由nofairsync和fairsync实现的,unlock方法是abstractqueuesynchronizer实现的,同时tryRelease是有sync实现的,tryAcquire是由nofairsync和fairsync实现的
Reentrantlock就是AbstractQueueSynchronizer的独占模式实现的,AbstractQueueSynchronizer的独占模式是怎么实现的?
- 首先abstractQueuesynchronizer中的独占模式提供的模板方法是tryRelease tryAcquire
release acquire,这些方法是需要自己实现,Reentrantlock内部有三个内部类Sync实现了tryRelease noFairSync和FairSync实现了tryAcquire方法 - ReentrantLock中提供了公平锁和非公平锁两种,两个内部类来实现,在构造方法中通过指定true表示指定是实现公平锁还是非公平锁
总结:abstractqueuesynchronizer依靠同步队列和state变量实现对锁的获取和释放的管理,当前线程获取同锁失败的时候,abstractqueuesynchronizer会将该线程以及相关等待信息包装成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,需要注意的是:同一时刻获取锁的线程可能有很多也就是说同一时刻要加入到同步队列中的线程有很多,这个时候,要防止出现冲突使用CAS进行设置尾节点的操作。当锁释放的时候,会将头节点head指向的节点(首节点,首节点就是拥有当前锁的线程)的后继节点中的线程唤醒,让其尝试获取同步状态,如果获取锁成功就让当前节点成为首节点,需要注意的是:同步队列中的节点对应的线程获取锁遵守先进先出形式,同一时刻同步队列中只有首节点的后继节点有机会获取同步锁,只是同步队列中,除了同步队列还有其他线程存在着锁的竞争,到底是同步队列中的节点获取了锁还是其他线程获取了锁是不一定的。但是设置首节点的操作是线程安全的,因为只有一个线程能够获取到锁,首节点是获取锁成功的线程设置的就是自身,因此设置首节点的操作不同于设置未节点的操作不需要CAS。
实例:
怎么获取同步状态,怎么释放同步状态,如何加入队列在总结中已经指明,代码层面上对应的实现是:
- 通过reentrantlock创建非公平锁,可以发现默认创建的就是非公平锁,也可以在new reentrantlock的时候指明为true还是false来决定创建公平锁还是非公平锁
public ReentrantLock(){
sync=new NonfairSync();// 默认创建的非公平锁
}
public ReentrantLock(boolean **fair**){//有构造函数的时候,根据fair变量是true还是false来判断
sync=fair?new FairSync():new NonfairSync();
}
- reentrantlock的内部类中nofairsync实现了非公平锁,fairsync实现了公平锁,都继承sync,实现了tryAcquire方法实现了sync的lock()方法,如下是nofairsync
static final class NonfairSync extends Sync{
final void lock(){
if(compareAndSetState(0,1)){ // 执行CAS操作,获取同步状态,就是获取锁
setExclusiveOwnerThread(Thread,currentThread()); // 将当前线程设置为独占锁线程
}else{
acquire(1);// 这个方法是同步队列器AQS中提供的方法,再次请求同步状态
}
}
}
这里表示获取锁,首先执行CAS操作尝试获取锁,尝试将同步队列器AQS中的state从0设置位1,如果返回true表示获取锁成功,如果返回false,表示已有线程持有这个锁此时state的值为1,获取锁失败,(这里可能同时存在多个线程设置state变量,不光是同步队列中的Node表示的线程,还有其他不在同步队列中的线程可能会同时获取锁),获取锁失败之后执行abstractqueuesynchronizer中的acquite(1)方法(acquire方法对中断不敏感,如果线程获取锁失败进入了同步队列,后面对该线程执行中断操作也不会从同步队列中移除,这和lockinterruptibly方法不同
public final void acquire(int arg){
if(!tryAcquire()&&acquireQueued(addWriter(Node.EXCLUSIVE),arg)
selfInterrupt();
}
acquire方法中的参数就是要设置的state,因为是要获取锁此时state的值为1,进入方法acquire方法之后会先执行tryAcquire方法,这个方法是在 AbstractQueuedSynchronizer中指定的模板方法,在AQS同步队列器中没有实现,是由nofairSync和fairSync中实现的,**在tryAcquire中调用了Sync类中的nofairtryAcquire(int acquires)方法,首先会尝试获取锁,如果获取成功了执行原子操作将其设置为独占式线程,如果state的值不为0说明已有其他线程或者当前线程占有了锁,判断是否是当前线程是否是重入锁,如果是重入锁将state的值加1,**此时设置state不用通过CAS原子操作,因为是同一个线程,只有一个线程获得了锁在执行相关操作。如果state状态为0表示没有线程获取到锁,此时会有多个线程获取此锁,这个时候要通过CAS原子操作设置state,如果是重入锁的情况就不用了
static final class NonfairSync extends Sync{
protected final boolean tryAcquire(int acquires){
return nofairTryAcquire (acquire);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer{
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();//获取当前线程
int c=getState();//获取当前线程的状态
if(c==0){
if(compareAndSetState(0,acquires)){// 如果当前线程获取同步状态成功,讲当前线程设置位独占式线程
setExclesiveOwnerThread(current);
return true;
}
}else if(current==getExclusiveOwnerThread()){
int nextc=c+acquires;
if(nextc<0){
Throw new Error;
}
setState(nextc);
return true;
}
return false;
}
}
3.如果当前线程没有获取到锁,执行acqurie方法中的tryAcquire(int args)返回false,此时将线程封装成节点添加到同步队列,接下来就是获取锁失败的线程如何添加到队列中,执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法中的addWaiter(Node.EXCLUSIVE)方法,会将线程封装称为节点添加到同步队列中,因为ReentrantLock属于独占锁
private Node addWaiter(Node mode){
Node node=new Node(Thread.currentThread,mode);// 将线程封装成一个节点
Node pred=tail; //pred指向尾节点
if(pred!=null){ // 如果同步队列不为空
node.prev=pred; // 将新节点的prev指向pred尾节点,此时尾节点还没有建立与新节点的联系
if(compareAndSetTail(pred,node)){ // 执行CAS原子操作,将尾节点执行该节点
pred.next=node;
return node;
}
}
// 如果尾节点为空是第一次加入,或者执行CAS操作的时候没有成功的将尾节点添加上,这个时候执行enque
enq(node);
return node;
}
需要注意的是:如果同步队列不为空,因为同一时刻可能有多个线程在获取锁,因此设置尾节点的时候会有冲突,需要通过CAS原子操作将节点添加到队尾。如果同步队列为空的话或者执行CAS操作失败的话会执行enq(Node),在enque中使用了一个死循环的CAS操作,如果是第一次添加,执行CAS设置到头部,如果不是第一次添加,执行CAS添加到尾部
private Node enq(final Node node){
for(;;){ //一直在循环
Node t=tail; // 指向队尾节点
if(t==null){ // 如果队列为空,没有头节点
if(compareAndSetHead(new Node())){ // 创建头节点并使用CAS设置头节点
tail=head; /// 从这里可以看到 head节点本身是不存放任何数据的,它只是作为一个牵头的节点,而tail永远指向尾部节点
}
}else{ / / 在队尾添加节点
node.prev=t;
if(compareAndSetTail(t,node){ // 使用CAS设置为尾节点
t.next=node;
return t;
}
}
}
}
在enq(node)方法中如果尾节点tail为null,表示没有同步队列,则创建新节点并通过CAS操作将节点设置为头节点,并且tail也执行头节点head,如果队列已经存在,但是因为CAS设置尾节点失败,则将新节点Node添加到队尾,这个设置头节点和添加尾节点的过程中可能有多个线程正在执行,同一时刻只有一个线程修改设置head tail成功,其他线程会继续循环,直到加入同步队列成功,这里使用CAS原子操作进行头节点设置和尾节点tail替换可以保证线程安全。
上面解决了获取锁的线程如何添加到同步队列中,那么同步队列中的线程如何获取锁的?
添加到同步队列之后,节点就会进入一个自旋的过程。每个节点都在观察时机等待条件满足获取锁,然后从同步队列中退出并结束自旋。自旋操作是节点在加入到同步队列之后进行的一个死循环,在这个死循环中,会判断当前节点的前置节点是否为头节点并且当前线程是否获取锁成功,如果是头节点并当前线程获取锁成功,就设置当前线程为头节点并且跳出自旋跳出死循环,如果,如果前置节点是头节点,但是当前线程没没有释放锁,就判断是否要挂起线程,前置节点是当前线程就继续获取锁,在死锁中,如果前置节点不是头节点,是signal节点,那么也继续获取锁,如果是结束状态就忽略,如果前置节点为condition,就转换为signal后继续等待,这几种情况都不会跳出自旋,只有当前节点的前置节点为头节点并且当前线程获取到了锁之后才会跳出循环,其他的时候都不会跳出循环。
acquireQueued(addWriter(Node.EXCLUSIVE),arg)的方法中执行的。
final boolean acquireQueued(final Node,int arg) {
try {
boolean interruppted =false;
// 进入死循环()
for(;;) {
final Node p=Node.prodecessor(); //获取当前线程的前置节点
if(p==head&&tryAcquire(arg)){ //p节点为头节点的时候,当前线程尝试获取锁
setHead(node); // 如果当前线程获取到了锁就将当前线程所在的节点设置为头节点
p.next=null;
failed=false;
return interruppted;
}
//如果前驱节点不是head,判断是否挂起线程
if(shouldParkAfterFaliedAcquire(p,node)&&parkAndInterrupt()) {
interruppted=true;
}
}
}finally {
if(failed) {// 如果挂起线程了就退出线程
cancelAcquire(node);
}
}
}
线程在自旋中(死循环)中获取同步状态,先获取当前节点的头节点,只有前置节点为头节点的时候才尝试后去获取同步状态,当头节点释放释放锁唤醒后继节点后,后继结点才有可能获取锁,因此当前节点判断前置节点是否为头节点,如果不是头节点,当前节点的线程将会被挂起
当前置节点为head节点并且已经释放同步状态时候,此时当前节点可能获取到同步状态,获取到 同步状态后,进入setHead方法,将当前节点设置为头节点在setHead()中
private void setHead(Node node){
head=node; // 头节点指向当前节点
node.thread=null; // 清空自己的线程情况
node.prev=null, // 让前置节点指向null
}
node节点被设置为head后,其thread信息和前驱节点将会被清空,因为线程已经获取到了锁,正在执行,没有必要存储相关的信息了,头节点(head指向的节点)只要保存指向后继节点的指针就可以,便于head节点释放锁之后唤醒后继节点。如果不是头节点就要判断是否要挂起线程,执行方法判断是否要挂起线程:
在if方法中判断是否要挂起线程,要同时满足两个条件的时候才挂起线程
首先要执行sholdparkafterfialedacquire(p,node)f方法
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
interrupted = true;
}
该方法的作用是判断当前节点的前驱节点是否为等待唤醒状态,如果为等待唤醒状态返回true,就是说要让他挂起来
private static boolean shouldParkAfterFailedAcquire(Node pred,Node node) {
// 获取当前节点的前置节点的状态
int ws==pred.waitStatus;
// 如果当前节点为等待唤醒状态返回true
if(ws==Node.singal) {
return true;
}
结束状态的值是大于0的,如果是结束状态的话,循环查找不是结束状态的节点,这样的话就将同步队列中为结束状态的节点全部的过滤掉,之后找到了节点之后返回false
if(ws>0) {// 如果当前节点的值>0说明是结束状态,遍历前驱节点直到找到不是结束状态的节点
do {
node.prev=pred=pred.prev;
}while(pred.waitstatus>0);
pred.next=node;
如果值小于0,并且前面已经确定不是signal状态,这个时候就是condition状态,这个状态表示当前线程正等待在一个condition上,这个时候,将节点中的状态原子性的设置为signal等待唤醒状态
}else {
compareAndSetWaitStatus(pred,ws,Node.SIGNAL);
}
除了当前节点为等待唤醒状态的情况返回true,为contion状态和为结束状态都返回false
return false;
}
private final boolean parkAndCheckInterrupt() {
//将当前线程挂起
LockSupport.part(this);
// 获取线程的中断状态,interrupted()是判断当前线程的中断状态,
return Thread.interrupted();
}
shouldparkafterfiledacquire()方法的作用是判断当前节点的前驱节点是否为等待唤醒状态,如果是等待唤醒状态返回true,如果当前节点的前驱节点大于0则为结束状态,说明前驱节点已经没有用了应该从同步队列中移除,执行while循环,直到找到不是结束状态的节点,如果当前节点的前驱节点不是结束状态也不是等待唤醒状态,说明为condition状态,从condition条件的等待队列中转移到同步队列上的时候,前驱节点的状态要从condition转换为等待唤醒状态。(需要注意的是,在判断是否挂起线程的方法中,这里说的说的前置节点是相对于方法之外的节点来说的)
如果执行shouldParkAfteFailedAcquire()方法判断前置节点为等待唤醒状态但是不是head节点返回了true,那么才会会继续执行parkandcheckinterrupt()方法,该方法中会将当前线程挂起,挂起之后变为等待状态,需要等待线程去唤醒他加入同步队列中。这里指的是等待的唤醒。
上面就是执行lock()加锁的内部原理。
需要注意的是,这张图中存在问题,添加节点的时候,如果不为空通过CAS添加到同步队列的队尾,如果为空设置头部节点,不用通过CAS,如果添加CAS失败会循环执行添加到队列末尾的操作。加入到同步队列中之后,进入了自旋死循环操作,在自旋中,如果前驱节点为head的话,当前线程才会去获得同步状态就获取同步状态,获得了锁之后会跳出循环。头节点不是同步节点,就会一直在自旋中,并且入如果前驱节点为signal状态,就不管,继续放在同步队列中排队,自旋,当前线程没有获取锁的机会(注意是当前线程),如果前驱节点不是signal 状态是condition状态就将前驱节点从等待队列中移动到同步队列中,并且从condition状态中转换为等待唤醒状态。前置节点为等待唤醒状态,当前线程要挂起到等待队列中进行等待,等待被唤醒加入到同步队列中。
上面讲述的方式是不可中断的获取锁,对于可中断的获取锁,是怎么实现的?
lock接口中提供了一个方法了可中断的获取锁的lockinterruptibly()方法或者是不阻塞的获取锁的trylock()在内部都调用了doAcquireInterruptibly()方法
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//直接抛异常,中断线程的同步状态请求
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在这个方法中最大的不同就是:前置节点不是头节点的时候,或者是头节点但是当前线程没有获取到锁,要执行判断是否要挂起线程操作,在这里,检测到线程的中断操作之后,会直接抛出异常,从而中断线程的同步状态,移除同步队列。
-------从当前节点不是头节点开始,理解的不到位。。。。。
lock()加锁的内部原理已经介绍,解锁操作是怎么完成的?
解锁操作发生在reentrantlock中的unlock()方法内,unlok()方法内部执行了abstractqueuedsynchronizer中的release方法,
ReentrantLock类中的unlock()方法
public void unlock(){
sync.release(1);// AQS同步队列器中的release方法
}
需要注意的是,这里的tryrelease是在Sync中实现的,tryRelease表示尝试释放锁,
public final boolean release(int arg){
if(tryRelease(arg)){ // 如果释放锁成功的话,清除头节点并且唤醒后继节点
Node h=head;
if(h!=null&&h.waitStatus!=0){
unparkSuccessor(h);
}
return true;
}
return false;
}
Sync中中实现了尝试释放锁的操作tryRelease(),要释放锁表示当前拥有锁,对应的release的值为1,获取锁的状态,可能是重入锁,对应的state的值可能不是1,而是大于1的数字,释放一个锁对应的值就减一。在tryRelease()方法中需要判断当前线程是否是独占锁拥有的线程。reentrantlock中没有state,只有在abstractqueuedsynchronizer中拥有state状态,就是锁lock实例中拥有state变量。判断当前锁的状态是否为0,如果为0表示已经释放了锁重新设置锁的状态。
protected final boolean tryRelease(int releases){
int c=getState()-releases;
if(Thread.currentThread()!=getExclusiveOwenerThread(){
throw new IllegalMonitorStateException();
}
boolean free=false;
if(c==0){
free=true;
//设置Owner为null
setExclusiveOwnerThread(null);// 解锁
}
setState(c); // 重新设置状态
return free;
}
锁已经释放了,锁已经释放了要进行唤醒其他线程获取锁的操作,这一步是在unparkSuccessor(h)中执行的。
释放同步锁之后会使用unparkSuccessor()方法唤醒后续的线程,首先要获取当前节点的状态waitstatus,将当前的状态变为0,之后找到下一个需要唤醒的节点,查看他的状态,如果是等待唤醒状态或者是condition状态,就将该节点对应的线程唤醒
private void unparkSuccessor(Node node){
int ws=node.waitStatus; // 获取当前线程所在的节点状态
if(ws<0){
compareAndSetWaitStatus(node,ws,0); //置零当前的**节点状态**,0为初始化状态,允许失败
}
Node s=node.next;// 找到下一个需要唤醒的节点s
if(s==null||s.waitStatus>0){ //如果为空或者已经取消
s=null;
for(Node t=tail;t!=null&&t!=node;t=t.prev){
if(t.waitStatus<=0){ // 小于0的节点是有效的节点
s=t;
}
}
if(s!=null){
LockSupport.unpark(s.Thread);// 唤醒线程
}
}
}
释放同步状态tryRelease(int release)方法在AQS同步队列器中定义的但是没有实现,在sync中实现的,ReentrantLock中的内部类有sync nonfairSync fairSync .释放同步状态之后会使用unparkSuccessor(h)唤醒后继节点的线程。
总结:释放同步锁唤醒后续节点的unparkSuccessor()方法中主要有两个内容,1.将当前节点设置为0,之后找到下一个状态正确的有效的需要唤醒的节点。2.执行unpark()操作唤醒线程。
upark()是唤醒同步队列中最前面的没有被放弃的线程。在前面,当线程的节点加入到同步队列的时候会进行自旋操作(死循环,只有当前节点的前置节点为头节点并且当前线程获取到了锁才能跳出循环)
unpark()是唤醒同步队列中最前边没有被放弃的线程。在前面,当线程的节点加入到同步队列的时候,会在acquireQueued方法中进行自旋(死循环,只有当前节点的前置节点为头节点,并且当前线程获取到了锁才能跳出循环) ,自旋过程中该线程会被唤醒后会进入acquiredQueued函数中的if (p == head && tryAcquire(arg)判断,如果该节点的前置节点不是头节点,会执行shouldParkAfterFailedAcquire()判断是否要进行挂起,因为节点通过unparkSuccessor()方法后已经是同步队列中最前边的未被放弃的线程节点,那么通过shouldParkAfterFailedAcquire()内部对结点状态的调整,s也必然会成为head的next节点,因此再次自旋的时候,p==head就成立了然后就把自己设置称为头节点,表示自己已经获取到了资源,最终acquire也会返回,这就是独占锁释放的全部过程
总结:独占锁释放的过程中,首先要释放锁,然后unpark唤醒同步队列中的线程去获取锁
总结关于lock锁实例:独占模式:非公平锁
关于独占模式的加锁和释放锁的过程已经分析完成,在abstractqueuesynchronizer中维护了一个同步队列,当线程获取同步状态失败之后,将会封装成为node节点加入到同步队列中并进行自旋操作,当当前线程的前驱节点为head的时候会尝试获取同步状态,也只有前驱节点为head的节点有机会获取同步状态,如果获取成功会将自己设置为head节点。在释放锁的时候,会通过调用Sync中的tryRelease(int release)方法释放同步状态,释放成功后将会唤醒后继节点的线程。
ReentrantLock中的公平锁
Reentrantlock中的公平锁的实现与非公平锁不同的是获取锁的时候公平锁的获取顺序严格按照时间上的FIFO规则,先请求的线程一定先获取到锁,后来的线程后获取到锁需要排队,公平锁获取锁的操作是在fairSync中的tryAcquire(release)中实现的,非公平锁是在nonfairTryAcquire中实现的。
公平锁FairSync类中的实现
tryAcquire方法和nonfairTryAcquire方法不同的是,在设置state值之前,调用了hasQueuedPredecessors() 方法判断同步队列中是否存在节点,如果存在节点必须要先执行完同步队列中的节点的线程,当前线程会被封装称为node节点加入到同步队列中进行等待。
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;
}
重入锁ReentrantLock是一个基于AQS同步队列器的并发控制类,内部类有sync nofairsync fairsync sync继承了AQS实现了释放锁的方法tryAcquire(int),而NoFairSync和FairSync都继承了Sync实现了获取锁的方法tryAcquire(int).ReentrantLock的所有方法的实现的都调用了这三个类
在使用synchronized还是condition的是时候要根据使用的场景来定,大部分情况下我们建议使用synchronized关键字,因为相对而言语义清晰并且虚拟机为 我们提供了自动的优化,而reentrantlock中提供了超时获取锁,中断获取锁,无阻塞获取锁,等待唤醒机制的多个条件变量,因此在使用的时候需要这些功能的时候可以选择ReentrantLock。