02_可重入锁(递归锁)+LockSupport+AQS源码分析

文章目录

  • ①. 可重入锁
  • ②. 为什么要使用LockSupport
  • ③. JUC强大的三个工具类
    • ①. CountDownLatch(闭锁)
    • ②. CyclicBarrier
    • ③.Semaphore(信号量)
  • ④. LockSupport详解
  • ⑤. AbstractQueuedSynchronizer之AQS
    • ①. AQS是什么?
    • ②. AQS内部体系架构
    • ③. ReentrantLock开始解读AQS
      • ①. 代码展示:
      • ②. 从最简单的lock方法开始看看公平和非公平
      • ③. lock()
      • ④. tryAcquire(arg)
      • ⑤. addWaiter(Node.EXCLUSIVE)
      • ⑥. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
      • ⑦. unlock( )获取permit

①. 可重入锁

1>. 可重入锁(递归锁)

  • ①. 指的是同一线程外层函数获得锁后,再进入该线程的内层方法会自动获取锁 (前提,锁对象是同一个对象)
    类似于家里面的大门,进入之后可以进入厕所、厨房等

  • ②. Java中ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁

  • ③. 隐式锁:(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法)
    原理如下:掌握

  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
  3. 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放
    在这里插入图片描述
//1.同步块
public class SychronizedDemo {
     
    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"+"内层....");
                   }
               }
           }
       },"A").start();
    }
    public static void main(String[] args) {
     
        new SychronizedDemo().sychronizedMethod();
        /*
        输出结果:
            A	外层....
            A	中层....
            A	内层....
        * */
    }
}
//2.同步代码块
class Phone{
     
    public synchronized void sendSms() throws Exception{
     
        System.out.println(Thread.currentThread().getName()+"\tsendSms");
        sendEmail();
    }
    public synchronized void sendEmail() throws Exception{
     
        System.out.println(Thread.currentThread().getName()+"\tsendEmail");
    }

}
/**
 * Description:
 *  可重入锁(也叫做递归锁)
 *  指的是同一线程外层函数获得锁后,内层递归函数任然能获取该锁的代码
 *  在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
 *  也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
 *  **/
public class ReenterLockDemo {
     
    /**
     * t1 sendSms
     * t1 sendEmail
     * t2 sendSms
     * t2 sendEmail
     * @param args
     */
    public static void main(String[] args) {
     
        Phone phone = new Phone();
        new Thread(()->{
     
            try {
     
                phone.sendSms();
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(()->{
     
            try {
     
                phone.sendSms();
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        },"t2").start();
    }
}
  • ④. 显示锁:(即lock)也有ReentrantLock这样的可重入锁
    (注意:有多少个lock,就有多少个unlock,他们是配对使用的;如果多一个或者少一个会使得其他线程处于等待状态)
class Phone2{
     
   static ReentrantLock reentrantLock=new ReentrantLock();

    public static void sendSms(){
     
        reentrantLock.lock();
        /*
        //reentrantLock.lock();
        注意有多少个lock,就有多少个unlock,他们是配对使用的
        如果多了一个lock(),那么会出现线程B一直处于等待状态
        * */
        reentrantLock.lock();
        try {
     
            System.out.println(Thread.currentThread().getName()+"\t"+"sendSms");
            sendEmails();
        }catch (Exception e){
     
            e.printStackTrace();
        }finally {
     
            reentrantLock.unlock();
        }
    }

    private static void sendEmails() {
     
        reentrantLock.lock();
        try {
     
            System.out.println(Thread.currentThread().getName()+"\t"+"sendEmails...");
        }catch (Exception e){
     
            e.printStackTrace();
        }finally {
     
            reentrantLock.unlock();
        }
    }
}
public class ReentrantLockDemo {
     
    public static void main(String[] args) {
     
        Phone2 phone2=new Phone2();
        new Thread(()->{
     phone2.sendSms();},"A").start();
        new Thread(()->{
     phone2.sendSms();},"B").start();
    }
}

②. 为什么要使用LockSupport

2>.为什么要使用LockSupport(先来了解下传统的等待唤醒机制)

  • ①. 3种让线程等待唤醒的方法
  1. 使用Object中的wait()方法让线程等待,使用Object中的notify方法唤醒线程
  2. 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
  • ②. Object类中wait( )和notify( )实现线程的等待唤醒
  1. wait和notify方法必须要在同步块或同步方法里且成对出现使用。 wait和notify方法两个都去掉同步代码块后看运行效果出现异常情况:
    Exception in thread “A” Exception in thread “B”
    java.lang.IllegalMonitorStateException
  2. 先wait后notify才可以(如果先notify后wait会出现另一个线程一直处于等待状态)
  3. synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法)
public class SynchronizedDemo {
     
    //等待线程
    public void waitThread(){
     
//      1.如果将synchronized (this){}注释,会抛出异常,因为wait和notify一定要在同步块或同步方法中
        synchronized (this){
     
            try {
     
                System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
                wait();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"end....");
        }
    }
    //唤醒线程
    public void notifyThread(){
     
        synchronized (this){
     
            System.out.println("唤醒A线程....");
            notify();
        }
    }
    public static void main(String[] args) {
     
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        new Thread(()->{
     

//            2.如果把下行这句代码打开,先notify后wait,会出现A线程一直处于等待状态
//            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            synchronizedDemo.waitThread();
        },"A").start();
        new Thread(()->{
     
            synchronizedDemo.notifyThread();
        },"B").start();
    }
}
  • ③. Condition接口中的await和signal方法实现线程等待和唤醒
    (出现的问题和object中wait和notify一样)
public class LockDemo {
     
    static Object object=new Object();
    public static void main(String[] args) {
     
        Lock lock=new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(()->{
     
            //如果把下行这句代码打开,先signal后await,会出现A线程一直处于等待状态
            //try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            lock.lock();
            try {
     
                System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
                condition.await();
            }catch (Exception e){
     
                e.printStackTrace();
            }finally {
     
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"END....");
        },"A").start();

        new Thread(()->{
     
            lock.lock();
            try {
     
                System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程****");
                condition.signal();
            }catch (Exception e){
     
                e.printStackTrace();
            }finally {
     
                lock.unlock();
            }
        },"B").start();
    }
}

③. JUC强大的三个工具类

3>. JUC强大的三个工具类 掌握

为什么这里要介绍下JUC强大的工具类?
CountDownLatch | CyclicBarrier | Semaphore 底层都是AQS来实现的

①. CountDownLatch(闭锁)

  • ①. CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞

  • ②. 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)

  • ③. 计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行

//需求:要求6个线程都执行完了,mian线程最后执行
public class CountDownLatchDemo {
     
    public static void main(String[] args) throws Exception{
     

        CountDownLatch countDownLatch=new CountDownLatch(6);
        for (int i = 1; i <=6; i++) {
     
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName()+"\t");
                countDownLatch.countDown();
            },i+"").start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t班长关门走人,main线程是班长");
    }
}
  • ④. 利用枚举减少if else的判断
public enum CountryEnum {
     

    one(1,"齐"),two(2,"楚"),three(3,"燕"),
    four(4,"赵"),five(5,"魏"),six(6,"韩");

    private Integer retCode;
    private String retMessage;

    private CountryEnum(Integer retCode,String retMessage){
     
        this.retCode=retCode;
        this.retMessage=retMessage;
    }

    public static CountryEnum getCountryEnum(Integer index){
     
        CountryEnum[] countryEnums = CountryEnum.values();
        for (CountryEnum countryEnum : countryEnums) {
     
            if(countryEnum.getRetCode()==index){
     
                return countryEnum;
            }
        }
        return null;
    }

    public Integer getRetCode() {
     
        return retCode;
    }

    public String getRetMessage() {
     
        return retMessage;
    }
}
/*
	楚	**国,被灭
	魏	**国,被灭
	赵	**国,被灭
	燕	**国,被灭
	齐	**国,被灭
	韩	**国,被灭
	main	**秦国一统江湖
* */
public class CountDownLatchDemo {
     
    public static void main(String[] args) throws Exception{
     
        CountDownLatch countDownLatch=new CountDownLatch(6);
        for (int i = 1; i <=6; i++) {
     
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName()+"\t"+"**国,被灭");
                countDownLatch.countDown();
            },CountryEnum.getCountryEnum(i).getRetMessage()).start();

        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t"+"**秦国一统江湖");
    }
}

②. CyclicBarrier

2>. CyclicBarrier

  • ①. CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法

  • ②. 代码验证:

    //集齐7颗龙珠就能召唤神龙
	public class CyclicBarrierDemo {
     
	    public static void main(String[] args) {
     
	        // public CyclicBarrier(int parties, Runnable barrierAction) {}
	        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
     
	            System.out.println("召唤龙珠");
	        });
	        for (int i = 1; i <=7; i++) {
     
	            final int temp=i;
	            new Thread(()->{
     
	                System.out.println(Thread.currentThread().getName()+"\t收集到了第"+temp+"颗龙珠");
	                try {
     
	                    cyclicBarrier.await();
	                } catch (InterruptedException e) {
     
	                    e.printStackTrace();
	                } catch (BrokenBarrierException e) {
     
	                    e.printStackTrace();
	                }
	            }).start();
	        }
	
	    }
	}

③.Semaphore(信号量)

3>. Semaphore(信号量)

  • ①. acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。

  • ②. release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

  • ③. 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

  • ④. 代码验证

	public class SemaphoreDemo {
     
	    public static void main(String[] args) {
     
	        Semaphore semaphore=new Semaphore(3);
	        for (int i = 1; i <=6; i++) {
     
	            new Thread(()->{
     
	                try {
     
	                    System.out.println(Thread.currentThread().getName()+"\t抢占了车位");
	                    semaphore.acquire();
	                    System.out.println(Thread.currentThread().getName()+"\t离开了车位");
	                } catch (InterruptedException e) {
     
	                    e.printStackTrace();
	                }finally {
     
	                    semaphore.release();
	                }
	            },String.valueOf(i)).start();
	        }
	    }
	}

④. LockSupport详解

4>. LockSupport详解

  • ①. 什么是LockSupport?
  1. 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
  2. LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
  3. 官网解释:
    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
    LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零
    可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
  • ②. 阻塞方法
  1. permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时, park方法会被唤醒,然后会将permit再次设置为0并返回。
  2. static void park( ):底层是unsafe类native方法
  3. static void park(Object blocker)

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第1张图片

  • ③.唤醒方法(注意这个permit最多只能为1)
  1. 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
  2. static void unpark( )
    02_可重入锁(递归锁)+LockSupport+AQS源码分析_第2张图片
  • ④. LockSupport它的解决的痛点
  1. LockSupport不用持有锁块,不用加锁,程序性能好
  2. 先后顺序,不容易导致卡死(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
  • ⑤. 代码演示:
/*
(1).阻塞
 (permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,
 park方法会被唤醒,然后会将permit再次设置为O并返回)
 static void park()
 static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
 (调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,
 permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
 static void unpark(Thread thread)
* */
public class LockSupportDemo {
     
    public static void main(String[] args) {
     

        Thread t1=new Thread(()->{
     
            System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
            LockSupport.park();
            /*
            如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
            所以下一行被注释的打开会导致程序处于一直等待的状态
            * */
            //LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
            },"A");
        t1.start();

        //下面代码注释是为了A线程先执行
        //try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}

        Thread t2=new Thread(()->{
     
            System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
            //有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
            LockSupport.unpark(t1);
            //LockSupport.unpark(t1);
        },"B");
        t2.start();
    }
}
  • ⑥. 面试题目:
  1. 为什么可以先唤醒线程后阻塞线程?(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
  2. 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?(因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行)

⑤. AbstractQueuedSynchronizer之AQS

①. AQS是什么?

  • ①. 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
    在这里插入图片描述

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第3张图片

  • ②. AQS为什么是JUC内容中最重要的基石
    (ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore )

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第4张图片

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第5张图片02_可重入锁(递归锁)+LockSupport+AQS源码分析_第6张图片
02_可重入锁(递归锁)+LockSupport+AQS源码分析_第7张图片

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第8张图片

  • ③. 锁,面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可)
    同步器,面向锁的实现者(比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。)

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

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第9张图片

②. AQS内部体系架构

  • ①. AQS内部架构图:

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第10张图片

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第11张图片

  • ②. 详解AQS内部代码有什么?
    02_可重入锁(递归锁)+LockSupport+AQS源码分析_第12张图片

  • ③. CLH队列(三个大牛的名字组成),为一个双向队列
    在这里插入图片描述

  • ④. 内部结构(Node此类的讲解)
    02_可重入锁(递归锁)+LockSupport+AQS源码分析_第13张图片

  • ⑤. 属性说明(Node此类的讲解)

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第14张图片

  • ⑥. AQS同步队列的基本结构

在这里插入图片描述

③. ReentrantLock开始解读AQS

写在最前面:
(1). 本次讲解我们走最常用的,lock/unlock作为案例突破口
(2). 我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?3个状态:没占用是0,占用了是1,大于1是可重入锁
(3). 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点)
业务图:
02_可重入锁(递归锁)+LockSupport+AQS源码分析_第15张图片

①. 代码展示:

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("-----A thread come in");

                    try {
      TimeUnit.MINUTES.sleep(20); }catch (Exception e) {
     e.printStackTrace();}
                }finally {
     
                    lock.unlock();
                }
        },"A").start();

        //第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
        //进入候客区
        new Thread(() -> {
     
            lock.lock();
            try{
     
                System.out.println("-----B thread come in");
            }finally {
     
                lock.unlock();
            }
        },"B").start();

        //第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
        //进入候客区
        new Thread(() -> {
     
            lock.lock();
            try{
     
                System.out.println("-----C thread come in");
            }finally {
     
                lock.unlock();
            }
        },"C").start();
    }
}

②. 从最简单的lock方法开始看看公平和非公平

  • ①. 通过ReentrantLock的源码来讲解公平锁和非公平锁

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第16张图片
在这里插入图片描述
在这里插入图片描述

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

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第17张图片

③. lock()

  • ①. lock.lock( ) 源码

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第18张图片

  • ②. acquire( ):源码和3大流程走向
    在这里插入图片描述

④. tryAcquire(arg)

  • ①.本次走非公平锁方向
    在这里插入图片描述
  • ②. nonfairTryAcquire(acquires)
    return false(继续推进条件,走下一步方法addWaiter)
    return true(结束)

在这里插入图片描述

⑤. addWaiter(Node.EXCLUSIVE)

假如3号ThreadC线程进来
(1). prev
(2).compareAndSetTail
(3).next

  • ①. addWaiter(Node mode )
    双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的
    在这里插入图片描述
  • ②. enq(node);
    在这里插入图片描述
  • ③. B、C线程都排好队了效果图如下:
    在这里插入图片描述

⑥. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

  • ①. acquireQueued
    (会调用如下方法:shouldParkAterFailedAcquire和parkAndCheckInterrupt | setHead(node) )

  • ②. shouldParkAfterFailedAcquire
    在这里插入图片描述

  • ③. parkAndCheckInterrupt
    在这里插入图片描述

  • ④. 当我们执行下图中的③表示线程B或者C已经获取了permit了

在这里插入图片描述

  • ⑤. setHead( )方法
    代码执行完毕后,会出现如下图所示
    在这里插入图片描述

02_可重入锁(递归锁)+LockSupport+AQS源码分析_第19张图片

⑦. unlock( )获取permit

  • ①. release | tryRelease | unparkSuccessor(h);
    在这里插入图片描述

  • ②. tryRelease()

在这里插入图片描述

  • ③. unparkSuccessor( )

在这里插入图片描述

你可能感兴趣的:(面试题第三季,1024程序员节)