Java开发常见面试题详解(LockSupport,AQS)

00_前言闲聊和课程说明

视频推荐

尚硅谷Java大厂面试题第3季,跳槽必刷题目+必扫技术盲点(周阳主讲)

01_字符串常量Java内部加载-上

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代的背景故事,在此我们就以测试代码来观察一下,使用"永久代"还是“元空间"来实现方法区,对程序有什么实际的影响。

String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。

代码

public class StringInternDemo {

	public static void main(String[] args) {
		
		String str1 = new StringBuilder("58").append("tongcheng").toString();
		System.out.println(str1);
		System.out.println(str1.intern());
		System.out.println(str1 == str1.intern());

		System.out.println();
		
		String str2 = new StringBuilder("ja").append("va").toString();
		System.out.println(str2);
		System.out.println(str2.intern());
		System.out.println(str2 == str2.intern());
		
	}

}

输出结果:

58tongcheng
58tongcheng
true

java
java
false

02_字符串常量Java内部加载-下

按照代码结果,Java字符串答案为false必然是两个不同的java,那另外一个java字符串如何加载进来的?

有一个初始化的Java字符串(JDK出娘胎自带的),在加载sun.misc.Version这个类的时候进入常量池。

递推步骤

System代码解析 System -> initializeSystemClass() -> Version

源码

package java.lang;

public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    
    //本地方法registerNatives()将会调用initializeSystemClass()
    private static void initializeSystemClass() {

		...
        
        sun.misc.Version.init();

		...
    }
    ...
}

package sun.misc;

//反编译后的代码
public class Version {
	private static final String launcher_name = "java";
	...
}

sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue〉做默认初始化,此时被sun.misc.Version.launcher静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

03_闲聊力扣算法第一题

04_TwoSum暴力解法

05_TwoSum优化解法

06_闲聊AQS面试

07_可重入锁理论

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

将字分开解释:

  • 可:可以

  • 重:再次

  • 入:进入

  • 锁:同步锁

  • 进入什么? - 进入同步域(即同步代码块/方法或显示锁锁定的代码

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。

自己可以获取自己的内部锁。

可重入锁的种类:

  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁。

    • 同步块
    • 同步方法
  • Synchronized的重入的实现机理。

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁。

08_可重入锁的代码验证-上

可重入锁的种类:

  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁。
    • 同步块
    • 同步方法

同步块代码演示

public class ReentrantLockDemo2 {
    Object object = new Object();

    public void sychronizedMethod(){
       new Thread(()->{
           synchronized (object){
               System.out.println(Thread.currentThread().getName()+"\t"+"外层....");
               synchronized (object){
                   System.out.println(Thread.currentThread().getName()+"\t"+"中层....");
                   synchronized (object){
                       System.out.println(Thread.currentThread().getName()+"\t"+"内层....");
                   }
               }
           }
       },"Thread A").start();
    }

    public static void main(String[] args) {
        new ReentrantLockDemo2().sychronizedMethod();
    }
    
}

输出结果:

Thread A	外层....
Thread A	中层....
Thread A	内层....

同步方法代码演示

public class ReentrantLockDemo2 {

    public static void main(String[] args) {
        new ReentrantLockDemo2().m1();
        
    }
    
    public synchronized void m1() {
    	System.out.println("===外");
    	m2();
    }
    
    public synchronized void m2() {
    	System.out.println("===中");
    	m3();
    }
    
    public synchronized void m3() {
    	System.out.println("===内");
    	
    }
}

输出结果:

=========

*09_可重入锁的代码验证-下

  • Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

显示锁代码演示

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Phone2 implements Runnable{

    Lock lock = new ReentrantLock();

    /**
     * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
     */
    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
        }
    }

    public void setLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        getLock();
    }
}

public class ReentrantLockDemo {


    public static void main(String[] args) {
        Phone2 phone = new Phone2();

        /**
         * 因为Phone实现了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

输出结果:

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock

10_LockSupport是什么

LockSupport是用来创建锁和其他同步类基本线程阻塞原语

LockSupport中的park()unpark()的作用分别是阻塞线程解除阻塞线程

总之,比wait/notify,await/signal更强。

3种让线程等待和唤醒的方法

  • 方式1:使用Object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
  • 方式2:使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

11_waitNotify限制

Object类中的wait和notify方法实现线程等待和唤醒

代码实现

public class WaitNotifyDemo {

	static Object lock = new Object();
	
	public static void main(String[] args) {
		new Thread(()->{
			synchronized (lock) {
				System.out.println(Thread.currentThread().getName()+" come in.");
				try {
					lock.wait();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		}, "Thread A").start();
		
		new Thread(()->{
			synchronized (lock) {
				lock.notify();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}
		}, "Thread B").start();
	}
}

waitnotify方法必须要在同步块或者方法里面且成对出现使用,如果两个都去掉同步代码块会抛出java.lang.IllegalMonitorStateException

原因:synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法)

调用顺序要先wait后notify才OK,否则会出现B线程先notify后,A线程后使用wait会出现另一个线程一直处于等待状态。

小总结

  • wait和notify方法必须要在同步块或者方法里面且成对出现使用
  • 先wait后notify才OK

12_awaitSignal限制

Condition接口中的await后signal方法实现线程的等待和唤醒,与Object类中的wait和notify方法实现线程等待和唤醒类似。

代码演示

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionAwaitSignalDemo {
		
	public static void main(String[] args) {
		
		ReentrantLock lock = new ReentrantLock();
		Condition condition = lock.newCondition();
		
		new Thread(()->{
			
			try {
				System.out.println(Thread.currentThread().getName()+" come in.");
				lock.lock();
				condition.await();				
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
			
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		},"Thread A").start();
		
		new Thread(()->{
			try {
				lock.lock();
				condition.signal();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}finally {
				lock.unlock();
			}
		},"Thread B").start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

await和signal方法必须要在同步块或者方法里面且成对出现使用,如果不进行lock加锁、放锁会抛出java.lang.IllegalMonitorStateException。

调用顺序要先await后signal才OK,否则B线程先进行释放锁,A线程再加锁,线程一直处于等待状态!

总结

传统的synchronized和Lock实现等待唤醒通知的约束必须满足下列条件:

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

13_LockSupport方法介绍

传统的synchronized和Lock实现等待唤醒通知的约束

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

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

可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

park()/park(Object blocker)阻塞当前线程阻塞传入的具体线程

源码

public class LockSupport {

    ...
    
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    ...
    
}

permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回

unpark(Thread thread)唤醒处于阻塞状态的指定线程

源码

public class LockSupport {
 
    ...
    
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    
    ...

}

调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回

14_LockSupport案例解析

案例主要是为了验证LockSupport加锁与解锁顺序无关。

案例代码

public class LockSupportDemo {

	public static void main(String[] args) {
		Thread a = new Thread(()->{
//			try {
//				TimeUnit.SECONDS.sleep(2);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
			System.out.println(Thread.currentThread().getName() + " come in. " + System.currentTimeMillis());
			LockSupport.park();
			System.out.println(Thread.currentThread().getName() + " 换醒. " + System.currentTimeMillis());
		}, "Thread A");
		a.start();
		
		Thread b = new Thread(()->{
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			LockSupport.unpark(a);
			System.out.println(Thread.currentThread().getName()+" 通知.");
		}, "Thread B");
		b.start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

正常 + 无锁块要求。

先前错误的先唤醒后等待顺序,LockSupport可无视这顺序。

重点说明

  • LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。

  • LockSuport是一个线程阻塞工具类所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

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

  • LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,

  • 调用一次unpark就加1变成1,

  • 调用一次park会消费permit,也就是将1变成0,同时park立即返回。

  • 如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证

形象的理解

  • 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

  • 当调用park方法时

    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出。
    • 如果无凭证,就必须阻塞等待凭证可用。
  • 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。

面试题

  • 为什么可以先唤醒线程后阻塞线程?

    因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

15_AQS理论初步

是什么?AbstractQueuedSynchronizer 抽象队列同步器

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

是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

这里的同步器组件指CountDownLatchSemaphoreReentrantLockReentrantReadWriteLock
在这里插入图片描述
CLH:Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO。

16_AQS能干嘛

AQS为什么是JUC内容中最重要的基石?

和AQS有关的
在这里插入图片描述

在这里插入图片描述

锁和同步器的关系

  • 锁,面向锁的使用者 - 定义了程序员和锁交互的使用层APl,隐藏了实现细节,你调用即可

  • 同步器,面向锁的实现者 - 比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

能干嘛?

加锁会导致阻塞 - 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理

解释说明

抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport park)的方式,维护state变量的状态,使并发达到同步的控制效果。

在这里插入图片描述

17_AQS源码体系-上

官网解释

提供一个框架来实现阻塞锁和依赖先进先出(FIFO)等待队列的相关同步器(信号量、事件等)。此类被设计为大多数类型的同步器的有用基础,这些同步器依赖于单个原子“int”值来表示状态。子类必须定义更改此状态的受保护方法,以及定义此状态在获取或释放此对象方面的含义。给定这些,这个类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态字段,但是只有使用方法getState()setState(int)compareAndSetState(int,int)操作的原子更新的’int’值在同步方面被跟踪。

在这里插入图片描述

有阻塞就需要排队,实现排队必然需要队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node,节点来实现锁的分配,通过CAS完成对State值的修改

源码

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

    private static final long serialVersionUID = 7373984972572414691L;

     * Creates a new {@code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() { }

     * Wait queue node class.
    static final class Node {

     * Head of the wait queue, lazily initialized.  Except for
    private transient volatile Node head;

     * Tail of the wait queue, lazily initialized.  Modified only via
    private transient volatile Node tail;

     * The synchronization state.
    private volatile int state;

     * Returns the current value of synchronization state.
    protected final int getState() {

     * Sets the value of synchronization state.
    protected final void setState(int newState) {

     * Atomically sets synchronization state to the given updated
    protected final boolean compareAndSetState(int expect, int update) {
         
    ...
}         

18_AQS源码体系-下

AQS自身

AQS的int变量 - AQS的同步状态state成员变量

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

    ...

     * The synchronization state.
    private volatile int state;
    
    ...
}

state成员变量相当于银行办理业务的受理窗口状态。

  • 零就是没人,自由状态可以办理

  • 大于等于1,有人占用窗口,等着去

AQS的CLH队列

  • CLH队列(三个大牛的名字组成),为一个双向队列

  • 银行候客区的等待顾客

在这里插入图片描述
官方解释

等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。CLH锁通常用于旋转锁。相反,我们使用它们来阻止同步器,但是使用相同的基本策略,即在其节点的前一个线程中保存一些关于该线程的控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。当一个节点的前一个节点释放时,它会发出信号。否则,队列的每个节点都充当一个特定的通知样式监视器,其中包含一个等待线程。状态字段并不控制线程是否被授予锁等。如果线程是队列中的第一个线程,它可能会尝试获取。但是,第一并不能保证成功,它只会给人争取的权利。因此,当前发布的内容线程可能需要重新等待。
要排队进入CLH锁,您可以将其作为新的尾部进行原子拼接。要出列,只需设置head字段。

小总结

  • 有阻塞就需要排队,实现排队必然需要队列

  • state变量+CLH变种的双端队列

AbstractQueuedSynchronizer内部类Node源码

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

    ...

     * Creates a new {@code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() { }

     * Wait queue node class.
    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;
        
        //等待condition唤醒
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        
        //共享式同步状态获取将会无条件地传播下去
        * waitStatus value to indicate the next acquireShared should     
        static final int PROPAGATE = -3;

        //当前节点在队列中的状态(重点)
        //说人话:
        //等候区其它顾客(其它线程)的等待状态
        //队列中每个排队的个体就是一个Node
        //初始为0,状态上面的几种
         * Status field, taking on only the values:
        volatile int waitStatus;

        //前驱节点(重点)
         * Link to predecessor node that current node/thread relies on
        volatile Node prev;

        //后继节点(重点)
         * Link to the successor node that the current node/thread
        volatile Node next;

        //表示处于该节点的线程
         * The thread that enqueued this node.  Initialized on
        volatile Thread thread;

        //指向下一个处于CONDITION状态的节点
         * Link to next node waiting on condition, or the special
        Node nextWaiter;

         * Returns true if node is waiting in shared mode.
        final boolean isShared() {

        //返回前驱节点,没有的话抛出npe
         * Returns previous node, or throws NullPointerException if null.
        final Node predecessor() throws NullPointerException {

        Node() {    // Used to establish initial head or SHARED marker

        Node(Thread thread, Node mode) {     // Used by addWaiter

        Node(Thread thread, int waitStatus) { // Used by Condition
    }
	...
}

AQS同步队列的基本结构

在这里插入图片描述

*19_AQS源码深度解读01

从ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的。

ReentrantLock原理

Lock lock = new ReentrantLock();

在这里插入图片描述
从最简单的lock方法开始看看公平和非公平,先浏览下AbstractQueuedSynchronizerFairSyncNonfairSync类的源码。

在这里插入图片描述
在这里插入图片描述
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()

hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
在这里插入图片描述
hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

  • 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

  • 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

在这里插入图片描述

接下来讲述非公平锁的lock()

整个ReentrantLock 的加锁过程,可以分为三个阶段:

  • 尝试加锁;
  • 加锁失败,线程入队列;
  • 线程入队列后,进入阻赛状态。

20_AQS源码深度解读02(A线程进入-成功)

ReentrantLock的示例程序

带入一个银行办理业务的案例来模拟我们的AQS 如何进行线程的管理和通知唤醒机制,3个线程模拟3个来银行网点,受理窗口办理业务的顾客。

代码实现

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
	
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		
		//带入一个银行办理业务的案例来模拟我们的AQs 如何进行线程的管理和通知唤醒机制
		//3个线程模拟3个来银行网点,受理窗口办理业务的顾客

		//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
				try {
					TimeUnit.SECONDS.sleep(5);//模拟办理业务时间
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} finally {
				lock.unlock();
			}
		}, "Thread A").start();
		
		//第2个顾客,第2个线程---->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代B只能等待,
		//进入候客区
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
			} finally {
				lock.unlock();
			}
		}, "Thread B").start();
		
		
		//第3个顾客,第3个线程---->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代C只能等待,
		//进入候客区
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
			} finally {
				lock.unlock();
			}
		}, "Thread C").start();
	}
}


程序初始状态方便理解图

在这里插入图片描述
启动程序,首先是运行线程A,ReentrantLock默认是选用非公平锁

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

由于 state 的状态确实为 0,和 CAS 预估值一样,CAS 操作成功,将 state 值改为 1,同时将 Node 节点中的线程设置为当前 线程A

初始状态

在这里插入图片描述

第一个顾客来了之后

在这里插入图片描述

21_AQS源码深度解读03(B线程进入-成功)

轮到线程B运行

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

在这里插入图片描述
此时state的状态值为1,代表锁已经被占有,CAS失败,走acquire()方法

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

先走tryAcquire()方法,如果成功就返回ture,取反返回false,就不走后面的方法

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread(); //获取当前线程:此时是顾客B
    int c = getState(); //获取state状态,此时值为1
    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;
}

下面对这个类中的两个判断进行分别解读:

if (c == 0) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

情况1:顾客B此时走入大厅,发现柜台有人在办理业务,需要去候客区排队,刚准备坐下时,此时顾客A办理完成,就直接去窗口办理:判断当前state状态是否为0,如果为0,进行CAS操作,将state设置为1

else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) // overflow
        throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
}
//附上state+1的方法
protected final void setState(int newState) {
    state = newState;
}

情况2:顾客A办理完,准备起身走时,发现还有件事忘记了办理,又坐下进行办理:判断当前线程是否为线程A,如果是,将state的状态值+1,(可重入锁的实现

总结:这两种情形都是可以获取到锁,即走tryAcquire()方法时返回true

22_AQS源码深度解读04(B线程进入-入队)

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

addWaiter方法分析

但,此时两种情形都不满足:即返回false,取反为true,继续走后面的方法addWaiter(Node.EXCLUSIVE)

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

在这里插入图片描述

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);  //当前线程为B,节点为null
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail; //tail为null
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

将tail节点赋给pred节点,此时就为null,不会进入if,走入队方法enq(node)

private Node enq(final Node node) { //此时node为B顾客
    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为空节点,会进入if判断,通过CAS操作设置head头结点的指向Node空节点(此时Node节点即图中的傀儡节点,不储存数据,仅用于占位)

private final boolean compareAndSetHead(Node update) { //此时传入的update为一个Node空节点
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

在这里插入图片描述
然后再将head头结点的执行赋给tail尾结点的指向

tail = head;

在这里插入图片描述
完成后,不会走下面的else 分支。由于是自旋,继续从头开始

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;//此时tail执行空节点,即不为null
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;//将B线程的前指针指向t节点(这里即tail节点)所执行的节点(这里即空节点)
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

tail不为null,走else分支,
首先:

node.prev = t;//将B线程的前指针指向空节点

在这里插入图片描述
然后:

compareAndSetTail(t, node) //设置尾结点:将tail尾结点所执向的节点改为执向顾客B

在这里插入图片描述
然后:

t.next = node; //将空节点的next指针指向顾客B

在这里插入图片描述

最后:return结束自旋!

23_AQS源码深度解读05(C线程进入-入队)

此时,第三个顾客C,也来办理业务,同样也没有抢到锁,需要走到addWaiter(Node.EXCLUSIVE)方法

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; //tail节点执行顾客B
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

此时,tail节点执行顾客B,赋给pred节点,所以pred节点也执行B,即pred不为null,需要进入if判断

首先:

node.prev = pred;//将顾客C的头指针指向顾客B

在这里插入图片描述
然后:

compareAndSetTail(pred, node)//设置尾结点指向顾客C

在这里插入图片描述

最后:

pred.next = node;//将顾客B的后指针指向顾客C

在这里插入图片描述
发现没有顾客C没有走enq(node)方法,也就是说此时已有哨兵节点,不需要再去创建哨兵节点进行占位。

若还有其他顾客D、E…走这条路依然是这样。

24_AQS源码深度解读06(B、C线程阻塞)

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

acquireQueued方法分析

虽然顾客B和顾客C依次都入了队,但是,没有真正的阻塞,下面开始执行acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) { //此时传入进的node为顾客B
    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);
    }
}

这里又是一个自旋

首先:

final Node p = node.predecessor();//设置p为哨兵节点

//附上源码
final Node predecessor() throws NullPointerException {
    Node p = prev;//prev为头指针,将其指向的节点付给p
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

然后:p=head相等,进入tryAcquire方法,再次尝试获取锁,假设现在依然抢不到锁,不能继续往下走,进入下一个if判断

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//此时pred为哨兵节点
    int ws = pred.waitStatus;//此时为0
    if (ws == Node.SIGNAL)//-1
        /*
             * 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;
}

进入:

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//设置waitStatus为-1

由于是自旋,再次进入acquireQueued,尝试获取锁,获取失败,同理又进入shouldParkAfterFailedAcquire方法,但此时waitStatus值为1,所以进入下列if

if (ws == Node.SIGNAL)
    /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
    return true;

shouldParkAfterFailedAcquire返回为true,继续向下执行

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;


//此时:真正被阻塞
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

这个时候,才调用park()方法,将线程进行阻塞!!!

在这里插入图片描述
在这里插入图片描述
顾客C同理,都被阻塞在这里,直到拿到许可证,才可被依次放行

25_AQS源码深度解读07(A线程结束)

unlock()方法

此时顾客A办理完业务,准备释放锁,走到tryRelease方法

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//此时c就为0
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null); //将当前拥有锁的线程设置为null
    }
    setState(c); //同时设置state值为0
    return free;
}

返回true,进入release方法的if语句

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//将头节点赋给h
        if (h != null && h.waitStatus != 0)//h的waitStarus状态值为-1
            unparkSuccessor(h);
        return true;
    }
    return false;
}

进入unparkSuccessor(h)方法:

private void unparkSuccessor(Node node) {
    /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
    int ws = node.waitStatus;  //此时为-1
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); //进入,通过CAS操作将状态设置为0

    /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
    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) //upark唤醒线程
        LockSupport.unpark(s.thread);
}

在这里插入图片描述

此时,顾客B和顾客C正挂起阻塞着,这里unpark后,相当于给了一张许可证

顾客B来个回马枪!!!

顾客B再次来到这个方法

final boolean acquireQueued(final Node node, int arg) { //顾客B
    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);
    }
}

尝试获取锁tryAcquire,来到nonfairTryAcquire方法

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的状态值为0,顾客B进入if判断

if (c == 0) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

将锁的线程设置当前线程B,返回true

即进入下列if判断

if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

首先:

setHead(node);//设置头节点


//附上源码
private void setHead(Node node) {
    head = node; //将头节点指向顾客B
    node.thread = null; //将顾客B的线程设置为null
    node.prev = null;//前指针设置为null
}

在这里插入图片描述
然后:将哨兵节点的后指针设置为null,此时哨兵节点等待垃圾回收

p.next = null; // help GC

在这里插入图片描述
此时原顾客B节点就成为新的哨兵节点

同理,顾客C出队也是如此操作!!!

AQS考点

# 第一个考点我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?

答 3个状态:没占用是0,占用了是1,大于1是可重入锁

# 第二个考点 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?
答案是3

思维导图

参考博客

系列一

系列二

系列三

系列四

系列五

系列六(视频版)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值