哈啰--AQS

AQS

关于多线程,不论是面试还是工作当中,它绝对算的上是重灾区,而我们使用多线程的目的说白了就是想要发挥多处理器的作用,提高程序性能和响应速度。

然而在多线程环境下,会存在一定的线程安全问题,为了保证高并发场景下的线程安全,绕不开的就是同步与锁机制,但它实现起来非常复杂且容易出现问题。

这个时候有位神人看不下去了,它鼻梁挂着眼镜,留着一嘴的白胡子,脸上挂着看起来有点腼腆的笑容,如果Java的历史,是以人为主体串接起来的话,那么一定少不了这个老头,他就是Doug Lea,我们平时开发过程中使用的JUC(java.util.concurrent )并发包,就是由他老人家主导设计的,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,大大的提高了 Java 并发编程的易用性。

好用归好用,我们大多数应该是不满足于做一个API调用的程序员,所以带着好奇心,我们需要了解一下它底层的实现原理。

AQS核心思想

在分析JUC的时候,绕不开的一个类就是 AbstractQueuedSynchronizer这个抽象类,我们简称为AQS框架,它为不同场景提供了实现锁及同步方式,为同步状态的原子性管理、线程的阻塞、线程的解除阻塞及排队管理提供了一套通用的机制,是实现 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等类的基础。

既然可以看作是个框架,那么它一定有它的设计思想在里面,AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,同时将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

那么它是怎么实现的呢?

AQS主要就是维护了一个volatile int型的state属性来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改,详细说明如下:

  • state表示同步状态,0表示自由状态,即未被占用;大于0表示占用状态,对state的更新必须要保证原子性。
  • 有阻塞就需要排队,实现排队必然需要队列,这里的队列是一个双向链表,元素的结点类型为Node类型,可以把node看作就是我们阻塞的线程,其中Node中的thread用来存放进入AQS队列中的线程引用,每个Node里面都有一个prev和next,它们分别是前一个节点和后一个节点的引用,如图所示

在这里插入图片描述

AQS资源共享方式分为:

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。

AQS能干嘛

我们刚才说ReentrantLock 、CountDownLatch、ReentrantReadWriteLock 、Semaphore 等等的实现都和AQS有关,源码为证,如图所示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

锁的实现方式

在分析AQS源码之前,我们先来回忆一下,关于锁的实现方式和线程间的通信方式。

我们常用锁的实现方式有两种,一种是使用synchronized同步代码块实现,另一种就是用JUC并发包中的Lock实现,二者都支持可重入,也就是我们经常听到的可重入锁。

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞,其优点是可一定程度避免死锁,下面我们通过代码来验证一下,

测试代码:

public class LockTest {
    static Object object=new Object();

    static  Lock lock=new ReentrantLock();

    static void printf(){
        synchronized (object){
            System.out.println(Thread.currentThread().getName()+"进入了synchronized  1");
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"进入了synchronized  2");
                synchronized (object){
                    System.out.println(Thread.currentThread().getName()+"进入了synchronized  3");

                }

            }
        }
    }

    static  void printf2(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"进入了 lock  11");
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"进入了 lock  22");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+"进入了 lock  33");

                }finally {
                    lock.unlock();
                }
            }finally {
                lock.unlock();
            }
        }finally {
            lock.unlock();//lock 和unlock要匹配;如果把该行注释的话,那么第一个线程正常运行没有问题,
                         // 第二个线程会迟迟获取不到锁
        }
    }

    public static void main(String[] args) {

        new Thread(() -> printf(),"线程 1").start();
        new Thread(() -> printf(),"线程 2").start();

        new Thread(() -> printf2(),"线程 1").start();
        new Thread(() -> printf2(),"线程 2").start();





    }
}

打印结果 :

在这里插入图片描述

线程的通信方式

回忆过锁的两种实现方式后,我们再来回顾一下线程间的通信方式,也就是让线程等待和唤醒线程的方式,基于刚才提到过的两种实现锁的方式,它们各自实现线程间通信的方式也不同,我们分别来看下:

1、wait/notify

使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程,但是在使用时需要注意以下两个问题:

  • wait和notify只能在synchronized代码块中且成对使用,不能单独使用;
  • 必须先wait然后再notify,否则会出现无人唤醒导致死等的问题

直接上代码说明:

public class WaitAndNotifyTest {
    static  Object lockObject =new Object();
    public static void main(String[] args) {
        new Thread(() -> {
           // try {                         
            //    Thread.sleep(3000);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
           // }
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName()+"进来了");
                try {
                    lockObject.wait();
                    System.out.println(Thread.currentThread().getName()+"被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName()+"唤醒");
                    lockObject.notify();
            }
        },"B").start();
    }
}

上面的例子很简单,两个线程,A调用wait(),然后B调用notify()唤醒,正常运行下没有问题,打印结果如下

在这里插入图片描述

1.1、要先wait后notify

如果我们把线程A中的sleep注释去掉的话,即B线程先notify,然后A再wait,此时就会出现A一直wait而没有人唤醒导致程序无法退出的问题,如图

在这里插入图片描述

1.2、必须在synchronized中使用

再一个,wait和notify需要在synchronized代码块中使用,如果把synchronized部分注释掉的话,即直接使用wait和notify时,程序运行会报错,如图

在这里插入图片描述

第15行和第26行对应代码分别为,lockObject.wait()和lockObject.notify(),看wait源码中的注释就知道了,要求wait必须在synchronized代码块中使用,notify也是如此。

在这里插入图片描述

2、await/signal

使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程,在使用时同样需要注意以下两个问题:

  • await和signal只能在lock和unlock中且成对使用,不能单独使用;

  • 必须先await然后再signal,否则会出现无人唤醒导致死等的问题

同样,直接上代码

public class AwaitAndSignalTest {
    static Lock lock=new ReentrantLock();
    static Condition condition=lock.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
//            try {
//                Thread.sleep(3000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"进来了");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"A").start();


        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"唤醒");
                condition.signal();
            }finally {
                lock.unlock();
            }
        },"B").start();

    }
}

上面代码通过lock和condition实现线程间的通信,同样两个线程,A调用await(),然后B调用signal()唤醒,正常运行下没有问题,打印结果如下

在这里插入图片描述

2.1、要先await后signal

这里面同样把sleep的注释放开,即B线程先signal(),然后A再await(),此时就会出现A一直await而没有人唤醒导致程序无法退出的问题,如图

在这里插入图片描述

2.2、必须在lock中使用

另一个问题,await和signal需要在lock代码块中使用,如果把lock部分注释掉的话,程序运行会报错,如图

在这里插入图片描述

3、LockSupport

LockSupport是一个线程阻塞工具类,用于创建锁和其它同步类的基本线程阻塞原语,所有的方法都是静态方法,其中的park和unpark方法的作用分别是阻塞线程和解除阻塞线程,可以让线程在任意位置阻塞并唤醒,LockSupport底层调用的是Unsafe中的native代码。

使用LockSupport时没有锁块的要求,前面介绍的两种通信方式中关于“ 先唤醒后等待 ”时发生的问题,使用LockSupport后不会再发生,我们直接来看下它的阻塞和唤醒是如何实现的:

public class LockSupportTest {
    public static void main(String[] args) {
        Thread a= new Thread(() -> {
            //try {
            //    Thread.sleep(3000);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
            //}
            System.out.println(Thread.currentThread().getName()+"进来了");
            LockSupport.park();
            //LockSupport.park();

            System.out.println(Thread.currentThread().getName()+"被唤醒");

        },"A");
        a.start();



        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"唤醒");
            LockSupport.unpark(a);
            //LockSupport.unpark(a);
        },"B").start();
    }
}

同样的功能,之前的两种方式都需要有锁块才能执行等待和唤醒的操作,使用lockSupport后,发现不需要使用锁块就能实现线程的等待和通知,运行下看有没有问题,打印结果如下

在这里插入图片描述

3.1、唤醒和等待没有先后顺序的要求

发现确实不需要加锁了,那么我们再来看看先unpark唤醒再park等待,把上面注释的sleep放开后,看看会不会发生同样的问题

在这里插入图片描述

可以看到,之前关于先唤醒后等待发生的问题,使用Locksupport后没有再发生。

3.2、LockSupport实现原理

LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程:

LockSupport类使用了一种名为Permit (许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。

可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
调用一unpark就加1变成1,
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
每个线程都有一个permit, 且最多只有一个,重复调用unpark也不会累积凭证。

下面通过演示来理解unpark不会积累凭证这个问题,如果把代码中注释的 LockSupport.park();和LockSupport.unpark(a);放开后,再执行,此时会发生阻塞的问题,即第一个LockSupport.park();会被唤醒,而第二个LockSupport.park();无法被唤醒,因为下面虽然连续调用了两次unpark,凭证仍然为1,打印结果如图

在这里插入图片描述

除非我们能保证执行一个park,对应执行一个unpark;然后再执行park和unpark,代码示例如下

package lockTest;

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest {
    public static void main(String[] args) {
        Thread a= new Thread(() -> {
           
            System.out.println(Thread.currentThread().getName()+"进来了");
            LockSupport.park();
            try {  //确保执行park后,执行b线程的unpark
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();

            System.out.println(Thread.currentThread().getName()+"被唤醒");

        },"A");
        a.start();



        new Thread(() -> {
            try {			//确保让A线程先执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName()+"唤醒");
            LockSupport.unpark(a);
            try {  //确保第一次unpark后,执行A线程的park
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(a);
        },"B").start();
    }
}

Node类

我们前面提到过,阻塞队列中节点类型为Node,它是AQS中的一个内部类,作用就是以队列的方式来存放线程节点,关于Node类中的成员变量我们需要认识一下,源码如下

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
...

	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 final int CONDITION = -2;
       
		//当前线程处于SHARED即共享式下才会使用该字段
        static final int PROPAGATE = -3;
        
        //初始为0,表示队列中线程的状态
        volatile int waitStatus;
        
		//前指针
        volatile Node prev;

        //后指针 
        volatile Node next;

        //表示处于该节点的线程
        volatile Thread thread;
        
		//指向下一个处于CONDITION状态的节点
        Node nextWaiter;
         
		...
	}     

	...

}    

AQS源码阅读

有了上面的前置知识以后,我们再回过头来继续研究AQS,从最常用的ReentrantLock开始下手。

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的,比如ReentrantLock中静态类sync,我们在调用lock.lock()的时候,实际上调用的是sync.lock();调用lock.unlock()的时候,底层调用的是sync.release(1);

所以表面上我们使用的是ReentrantLock,而实际上使用的是Sync这个类,Sync继承自AbstractQueuedSynchronizer,也就是AQS,完成公平锁和非公平锁的操作,如图所示

在这里插入图片描述

单独的一句 Lock lock=new ReentrantLock(); 我们知道,默认是非公平锁,

public class ReentrantLock implements Lock, java.io.Serializable {	
	private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }
	
    //和传false是一样的效果
	public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

顺便看下公平锁和非公平锁的源码实现

//非公平锁
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;
}


//公平锁
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;
        }

可以明显看出公平锁与非公平锁的唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()方法,它是公平锁加锁时判断等待队列中是否存在有效节点的方法,即判断是否需要排队。

导致公平锁和非公平锁的差异如下:

  • 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
  • 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark() 之后还是需要竞争锁(存在线程竞争的情况下)

整个ReentrantLock的加锁过程,可以分为三个阶段:
1、尝试加锁;
2、加锁失败,线程入队列;
3、线程入队列后,进入阻塞状态。

下面我们就通过模拟银行窗口办理业务的情景,来跟踪一下AQS底层的实现细节,代码如下,

/**
 * 通过ReentrantLock 模拟银行窗口办业务,分析AQS源码流程
 */

public class AQSByRTLock {
    static Lock  lock=new ReentrantLock();
    public static void main(String[] args) {

        //A是第一个客户,此时窗口没有人办理业务,A可以直接办理
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("A客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"A").start();

        //B是第二个客户,由于只有一个窗口且此时有人在办理业务,B需要进入等候区进行等待
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("B客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"B").start();


        //C是第三个客户,C也需要进入等候区进行等待,等A办完之后,可以和B争抢办理业务
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("C客户在办理业务");

                Thread.sleep(30000);

            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        },"C").start();
    }
}

假设A线程先来,此时业务窗口没有人,A可以直接去办理,后面的B和C只能等待,那么此时A执行流程如下:

一、lock方法

A执行lock.lock()方法,实际底层执行的是sync.lock()方法;由于默认是非公平锁,所以来到的是NonfairSync.lock()方法,源码如下

	
public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
    
	static final class NonfairSync extends Sync {
        ...
            
		final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        ...
    }
}
	
1.1、compareAndSetState

此时A进来后,它不知道现在是否轮到自己处理业务(也就是不知道state现在的状态是否为0),所以它通过compareAndSetState这个方法去尝试一下,它期待的值是0,也就是窗口当前没有人办理业务;如果是0的话,我就把它改为1,表示窗口有人在办理业务;

当前没有任何人办理业务,所以A如愿以偿,进入窗口办理业务,此时state修改为1

public abstract class AbstractQueuedSynchronizer  extends AbstractOwnableSynchronizer {

    ....
    
	//父类AbstractQueuedSynchronizer中的方法,底层就是调用unsafe类的compareAndSwapInt方法
	protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
    ...
    
}
1.2、setExclusiveOwnerThread

然后执行setExclusiveOwnerThread方法,执行这个方法的目的就是记录一下,占用窗口的线程是谁,其对应源码如下

//AbstractQueuedSynchronizer  继承了	AbstractOwnableSynchronizer

public abstract class AbstractOwnableSynchronizer  implements java.io.Serializable {
    
    ...
    
	protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    
    ...
    
}

所以这个时候A就成功进入到窗口前进行办理业务了。

1.3、acquire

当A在窗口办理业务时,这时B来了,也就是B执行lock.lock()方法,和A一样,它执行的也是sync.lock()方法,最终来到NonfairSync.lock()方法,它进来后也不知道现在自己是否能够办理业务,它也要尝试一下,通过compareAndSetState方法尝试对state进行修改,它期望的值也是0,然后修改为1 ;

这时候,state已经被A修改为1了,所以尝试失败,compareAndSetState返回false,B这个时候就知道,窗口已经有人在办理任务了;它只能等待,执行acquire方法

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    ...
    
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    ...
    
}

在acquire方法中,分别调用了三个方法,我们一个一个来看看这几个方法的作用

1.3.1、tryAcquire

B这个时候还抱有一丝幻想,尝试着抢占,万一A业务办理完了呢,所以调用tryAcquire方法来试试看,但是我们跟进去发现其源码长成这个样子,

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    
    ...
        
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    
    ...
                 
}

一行业务代码没有,直接抛个异常,这让我们看什么呢?

实际上这是一种典型的模板方法设计模式的应用场景,要求所有子类必须实现这个方法,如果没有实现那我就抛异常给你看看。

所以我们继续向下跟,看它的子类,这里我们选择在ReentrantLock中NonfairSync的实现,

在这里插入图片描述

跟进去发现它最终调用的是子类中Sync的nonfairTryAcquire方法,其源码如下所示

	
public class ReentrantLock implements Lock {
    
    ...
    
	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;
        }
    
    ...
    
}

流程解释如下:

  • 首先进来之后,先获取到当前操作的线程是谁;

  • 然后获取到当前的状态state值,看看现在窗口前是否还有人在办理业务(存在侥幸心理,即B刚来,A刚好执行完毕,此时state变成了0的情况);

  • 如果state为0呢,就说明现在未被占用,那么我就尝试去获取,也就是尝试对state值进行更改,此时很明显state不为0,所以继续往下走

  • state不为0,说明有人在窗口办理业务,那我再判断一下,看看当前窗口办理的那个人,是不是你(也就是判断下,正在窗口前办理业务的线程是不是B线程,为什么要做这个判断呢?实际上就是针对可重入锁的那种情况,如果你是重入的线程,那么state也需要进行加1操作),很明显不是,所以程序继续往下走。

  • 最后返回false

这个时候返回到acquire方法中的if判断 if ( !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),

由于 !tryAcquire(arg) 结果为true,所以需要继续向后执行,在调用acquireQueued方法之前,先调用addWaiter方法,所以我们下面来看addWaiter(Node.EXCLUSIVE)的流程,这里再次明确一下,此时执行该方法的线程是B线程

1.3.2、addWaiter

调用addWaiter(Node.EXCLUSIVE)方法的时候,传入了一个EXCLUSIVE表示独占模式,当执行该方法的时候,就说明,线程需要入队排队了,

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
    
    ...
        
        
   	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;
    }
        
    ...
}

执行流程如下:

  • currentThread获取的就是当前线程B,因为它没有获取到锁,所以现在需要入队等侯,那么队列中的node实际上就是一个个的线程。

  • 因为队列中还没有任何的节点,所以此时tail尾指针指向的是null,所以此时不会进入if判断,而直接进入enq方法,参数node代表的就是B线程

1.3.2.1 、enq方法
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;
                }
            }
        }
    }
  • 由于现在队列没有节点,所以tail还是个null,也就是t为null,会进入if代码块
  • 通过compareAndSetHead方法,这里自动创建了一个新的node(称为哨兵节点,关于它的作用我们后面再介绍)作为head也就是头节点,创建完成之后,此时让头指针head和尾指针tail都指向刚刚创建的这个node节点,此时第一次for循环执行完毕,如图所示

双向链表中,也就是等候队列中,第一个节点为虚节点(也叫哨兵节点),它不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

在这里插入图片描述

  • 第一个node节点创建完成之后,开始第二次循环,此时tail不为null了,所以会执行else部分
  • node.prev = t;,t指向的是第一次循环创建的哨兵节点,把它赋值给node.prev;node是B,也就是NodeB的前指针让它指向哨兵节点,也就是把NodeB这个节点挂到哨兵节点的后面
  • compareAndSetTail(t, node);这句话就是要设置最新的尾节点,也就是要让tail指针指向NodeB这个节点
  • t.next = node; t实际上对应的就是哨兵节点,t.next就是让哨兵节点的next指针指向NodeB,
  • 最后返回t,此时就完成了NodeB节点的入队,对应代码完成的效果如下图所示,

在这里插入图片描述

后续节点要进入等候队列时,就不用像第一次这样麻烦了,比如C节点,只需要将tail尾指针指向后续进入队列的节点(tail指向C),同时把该节点的前指针指向入队前的尾节点也就是前尾节点(C.prev 指向B),然后让前尾节点的next指向该节点(B.next指向C)即可,如图所示

在这里插入图片描述

1.4、acquireQueued

那么B线程通过addWaiter之后呢就进入到等候队列里面了,接下来就该执行acquireQueued方法了,下面我们来分析下这个方法都干了什么事。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
  
    ...
        
    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);
        }
    }    
     
    ...
        
}

failed如果为true的话,最后会执行cancelAcquire取消排队;而interrupted如果为true的话,说明发生了故障,这里面我们不分析极端的情况,只看主流程部分,执行流程解析如下:

1.4.1、predecessor
  • 又是一个 for (;😉 自旋,首先第一句 final Node p = node.predecessor();这里的node我们要知道它代表的是NodeB,predecessor方法就是获取当前节点的前一个节点,此时NodeB的前节点是哨兵节点,所以这里的Node p 代表的是哨兵节点,predecessor源码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {	
    
    ...
    
	static final class Node {	
        
		...
            
		final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        
        ...
        
    }
  • p获取到值以后,继续向下,进入if判断,p此时和head的值一样,都指向哨兵节点,所以再次执行tryAcquire方法尝试获取锁,现在A仍然在办理业务,所以tryAcquire()返回false,所以不进入if代码块,继续向下走
1.4.2、shouldParkAfterFailedAcquire
  • 这时来到下一个if,先执行shouldParkAfterFailedAcquire方法,也就是在尝试抢占锁失败后执行的方法,

    • 需要明确的是,此时pred代表的是哨兵节点,node代表的是NodeB
    • 第一次执行的时候,pred.waitStatus为默认值0,所以直接执行else部分,将waitStatus改为Node.SIGNAL也就是-1,最终返回false;也就是 shouldParkAfterFailedAcquire()方法为false,第一次for循环结束。

在这里插入图片描述

  • 第二次for循环,还是重复刚才的步骤,尝试获取锁,如果没获取到还是会再次进入第二个if执行 shouldParkAfterFailedAcquire方法,这时候就跟刚才不一样了

  • 当再次进入shouldParkAfterFailedAcquire方法的时候,此时ws也就是哨兵节点的waitStatus已经在上一次循环的时候改为-1了,这时比较,ws == Node.SIGNAL为true,此时就会成功执行return true;即shouldParkAfterFailedAcquire方法返回值为true。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {	
    
    ...
        
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
}
1.4.3、parkAndCheckInterrupt

当shouldParkAfterFailedAcquire方法返回值为true的时候就可以执行 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())中的parkAndCheckInterrupt方法了。

在这里面我们看到了熟悉的LockSupport.park(),其中tihs就是指线程B,当执行这个方法的时候,才是真真正正的在等候队列里面坐稳了,也就是被阻塞了,直到被人唤醒。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {	
    
    ...
        
	private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    
    ...
        
}
    

同样的道理,C最后也是这也走一遍,如果一直获取不到锁,最后也会执行到这个方法当中,进行阻塞等待唤醒。

这里明确一下,B和C此时就在AbstractQueuedSynchronizer类中的acquireQueued方法中调用的parkAndCheckInterrupt方法这里等待被唤醒,如图所示

在这里插入图片描述

二、unlock方法
1、release

当A线程任务执行完毕后,它就要执行unlock方法来释放锁了,底层实际上调用的是sync.release(1);方法,源码如下

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {	
    
    ...
        
	public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    ...
}
    
1.1、tryRelease

首先执行tryRelease方法,跟进去发现又是个模板方法,轻车熟路,我们直接看它的子类,

public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
        
    abstract static class Sync extends AbstractQueuedSynchronizer {
        
        ...
        
        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;
        }
        
        ...
        
    }
    
    ...
    
}    
        
        
        

tryRelease流程分析:

  • releases我们传的默认值为1 ,先获取state值,此时state值为1,1-1=0,所以c=0;
  • 第一个if判断一下,看看当前要释放锁的线程是不是当前正在办理业务的线程,如果不是则抛异常,这里当前线程为A,办理业务的线程也为A,所以满足,继续向下
  • free默认为false,表示处于被占用
  • 第二个if判断state-1之后的值是不是0,如果是0表处理完业务了,要释放了,所以此时将free置为true,表示共享资源处于空闲状态,同时把当前窗口办理业务的线程置为null,表示没有人办理业务
  • 最后更新state的值,也就是0;然后返回释放结果free,此时A完成了释放锁操作
1.2、unparkSuccessor

为了方便理解,这里把此时一个总的状态图,给大家画了一下,现在state状态为0,业务窗口此时没有线程办理业务,也就是说A线程已经办理完业务并且已经释放了资源,接下来就要对阻塞队列进行处理出栈的操作,也就是开始让B线程去办理业务了。

在这里插入图片描述

tryRelease执行完后,回到release方法,if判断tryRelease执行结果,此时返回true,程序进入if代码块继续向下运行,首先对head节点进行判断,由于head此时指向的是哨兵节点,所以不为null,且waitStatus != 0,此时会成功执行unparkSuccessor方法,该方法就是负责完成B线程出栈(这里的做法是让哨兵节点出栈,被垃圾回收器回收,然后将NodeB变成新的哨兵节点)及唤醒操作,源码如下

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{

    ...

	private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

    
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
    ...
    
}

unparkSuccessor方法流程分析:

  • 这里传进来的node是哨兵节点,首先对哨兵节点的WaitStatus进行判断,此时值为-1小于0,将WaitStatus设置为默认的0;
  • 然后获得哨兵节点指向的下一个节点是谁,也就是获取到要唤醒的那个线程,这里指的是NodeB,也就是s=NodeB
  • 第二个if判断,由于s不为null且s.waitStatus=0,不满足条件,继续向下
  • 最后执行 LockSupport.unpark(s.thread)方法,唤醒s,也就是唤醒B线程
1.3、再回parkAndCheckInterrupt

这时候就该回到我们分析lock方法时的acquireQueued方法中调用的parkAndCheckInterrupt方法了,

在这里插入图片描述

再来回顾下parkAndCheckInterrupt方法的内部实现,在唤醒以前,B线程就卡在 LockSupport.park这里,当被唤醒后,才会继续向下执行,由于当前没有人执行过中断方法,所以Thread.interrupted()返回中断标志的默认值,false

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {	
    
    ...
        
	private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    
    ...
        
}
    

返回到哪了呢?估计大伙应该忘了,ok这里再来看下,返回到了哪里

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  {
  
    ...
        
    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);
        }
    }    
     
    ...
        
}

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())也就是返回到了这里,由于结果为false,所以进行下一次循环开始执行:

  • 这里的node我们需要明确,它代表的是B,此时B的前节点仍然是哨兵节点,并且头节点指向的仍是该哨兵节点
  • 再次执行tryAcquire方法进行获取锁,这次执行就不一样了,因为A已经办完业务走了,state为0,所以执行完tryAcquire方法后,state由0改为了1表示资源被占用,同时B线程成功进入到窗口开始办理业务,最后返回true;
  • 到这就完成了B线程的唤醒、出栈(逻辑上的出栈,此时阻塞队列有B,业务窗口前也有B)办理业务的工作;下面开始,也就是if{}中的内容,负责将队列种的B节点变为哨兵节点,也就是让前哨兵节点出栈
  • 首先重新设置头节点,即让head指向NodeB,然后将NodeB节点中的线程置为null,因为B线程已经去办理业务了;同时让NodeB的前指针置为null(原来NodeB的前指针指向前哨兵节点)
  • p.next = null;这里的P指的是前哨兵节点,将它的next置为null后,此时前哨兵节点就变成没有任何人指向它的孤立节点,不久就会被垃圾回收器回收掉。
  • failed = false;表示当前状态为正常处理,置为false后就不会执行finally中的cancelAcquire,也就是没有发生取消排队这回事
  • 最后返回interrupted为fasle表示没有被打断过,这样一整套流程就执行结束了,最终结果如图所示

在这里插入图片描述

到这我们就根据模拟银行窗口办理业务的方式,把获取锁、阻塞、唤醒这一套完整的流程就交代完了,学习ing,难免有理解不对的地方,望大佬们指正。

每日一皮

有些东西只想给你,就算你不要,我也舍不得给别人

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哈啰单车 app代码是指用于开发和构建哈啰单车移动应用程序的计算机代码。这些代码主要由程序员编写,以实现不同的功能和特性。 哈啰单车 app代码可以分为前端和后端两部分。 前端代码主要用于构建用户界面和用户交互体验。它通常使用HTML、CSS和JavaScript等前端开发语言来实现不同的页面布局、样式和功能。前端代码还可以使用Web框架(如React、Vue等)来更高效地开发和维护用户界面。通过前端代码,用户可以通过哈啰单车 app与系统进行交互,查看车辆位置、借还单车、支付费用等功能。 后端代码主要负责处理用户请求、管理数据库和与第三方服务进行交互。它通常使用后端开发语言(如Java、Python等)编写,并使用框架(如Spring、Django等)来简化开发过程。后端代码负责处理用户请求,执行相应的业务逻辑,并将结果返回给前端。它还与数据库进行交互,将用户数据存储和管理在数据库中。 哈啰单车 app代码还涉及到其他功能和特性的实现,如地图显示车辆位置、支付功能、推荐算法等。这些功能和特性需要使用相应的API和库来实现。 总之,哈啰单车 app代码是为了实现哈啰单车移动应用所编写的计算机代码。它包括前端和后端代码,用于构建用户界面、处理用户请求、管理数据库和与第三方服务进行交互。通过哈啰单车 app代码,用户可以享受方便快捷的单车共享服务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值