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

题目说明

可重入锁

可重入锁(递归锁)
  • ①. 指的是同一线程外层函数获得锁后,再进入该线程的内层方法会自动获取锁 (前提,锁对象是同一个对象)
    类似于家里面的大门,进入之后可以进入厕所、厨房等
  • ②. Java中ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
  • ③. 隐式锁:(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法)
  • 原理如下:掌握
  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
  3. 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放

可重入锁四个字分开解释:

可: 可以,重:再次,入:进入,锁:同步锁。进入什么?进入同步域,即同步代码块/方法或显示锁锁定的代码

总结:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。

可重入锁的种类
  • 隐式锁 synchronized关键字使用的锁,默认是可重入锁,同步代码块和同步方法,JVM层面的
    • Synchronized的重入的实现机理,自动挡的车
  • 显示锁 Lock 也有ReentrantLock这样的可重入锁,手动挡的车

①同步代码块:

//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	内层....
        * */
    }
}

②同步方法:

package com.interview.juc;

/**
 * Copyright (C), 2018-2020
 * FileName: ReenterLockDemo
 * Author:   kongfanyu
 * Date:     2021/1/25 14:20
 */
public class ReenterLockDemo {
    
    
    public synchronized void m1(){
    
    
        System.out.println("===外");
        m2();
    }
    public synchronized void m2(){
    
    
        System.out.println("===中");
        m3();
    }
    public synchronized void m3(){
    
    
        System.out.println("===内");
    }

    public static void main(String[] args) {
    
    
        new ReenterLockDemo().m1();
    }
}

从字节码角度分析synchronized实现
  • javap -c xxx.class 文件反编译
  • synchronized同步代码块
    • javap -c xxx.class 文件反编译
    • 在这里插入图片描述
    • 多出来的一个确保出现异常的时候可以退出,释放锁
  • synchronized普通同步方法
  • synchronized静态同步方法
Synchronized的重入的实现机理

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

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

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

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

答案在Java的对象头里。在对象头里,有一块数据叫Mark Word。 在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字 段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异,此处不再进一步讨论。此处主要是想说明锁实现的思路,因为后面讲ReentrantLock的详细实现时,也基于类似的思路。在这个基本的思路之上,synchronized还会有偏向、自旋等优化策略,ReentrantLock同样会用到这些优化策略,到时会结合代码详细展开。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:
在这里插入图片描述
对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间。

Java对象结构与锁实现原理及MarkWord详解

显示锁:

package com.interview.juc;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLockDemo {
    
    
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
    
    
       new Thread( ()->{
    
    
           lock.lock();
           try {
    
    
               System.out.println("外层----");
               lock.lock();
               try
               {
    
    
                   System.out.println("内层======");
               } finally {
    
    
                   lock.unlock();
               }
           }finally {
    
    
               lock.unlock();
           }
       },"线程A" ).start();
    }
}

如果加锁的次数和释放锁的次数不对应,就会出现等待状态,导致其他线程不能执行。

package com.interview.juc;

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

/**
 * Copyright (C), 2018-2020
 * FileName: ReenterLockDemo
 * Author:   kongfanyu
 * Date:     2020/11/25 14:20
 */
public class ReenterLockDemo {
    
    
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
    
    
       new Thread( ()->{
    
    
           lock.lock();
           try {
    
    
               System.out.println("外层----");
               lock.lock();
               try
               {
    
    
                   System.out.println("内层======");
               } finally {
    
    
                   //lock.unlock();//注释之后,线程B不能执行
               }
           }finally {
    
    
               lock.unlock();
           }
       },"线程A" ).start();
       //========================
        new Thread( () ->{
    
    
            lock.lock();
            try
            {
    
    
                System.out.println("线程B执行.......");
            } finally {
    
    
                lock.unlock();
            }

        },"线程B").start();
    }
}

另一个案例:

//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

LockSupport是什么?
java.util.concurrent.locks
public class LockSupport extends Object
用于创建锁和其他同步类的基本线程阻塞原语。
这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
方法park和unpark提供了阻止和解除阻塞线程的有效手段,该方法不会遇到导致不推荐使用的方法Thread.suspend和Thread.resume目的不能使用的问题:一个线程调用park和另一个线程之间的尝试unpark线程将保持活跃性,由于许可证。 另外,如果调用者的线程被中断, park将返回,并且支持超时版本。 park方法也可以在任何其他时间返回,因为“无理由”,因此一般必须在返回之前重新检查条件的循环中被调用。 在这个意义上, park作为一个“忙碌等待”的优化,不浪费时间旋转,但必须与unpark配对才能有效。

park的三种形式也支持blocker对象参数。 线程被阻止时记录此对象,以允许监视和诊断工具识别线程被阻止的原因。 (此类工具可以使用方法getBlocker(Thread)访问阻止程序 。)强烈鼓励使用这些形式而不是没有此参数的原始形式。 在锁实现中作为blocker提供的正常参数是this 。

这些方法被设计为用作创建更高级同步实用程序的工具,并且本身对于大多数并发控制应用程序本身并不有用。 park方法仅用于形式的构造:

   while (!canProceed()) {
    
     ... LockSupport.park(this); } 
其中既不canProceed也没有任何其他动作之前的呼叫park需要锁定或阻止。 因为只有一个许可证与每个线程相关联, park任何中介使用可能会干扰其预期效果。

重点: LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。类似wait和notify方法。

线程等待唤醒机制(wait/notify)

在这里插入图片描述
3种让线程等待和唤醒的方法

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

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

package com.interview01;

/**
 * Copyright (C), 2018-2020
 * FileName: Demo1
 * Author:   kongfanyu
 * Date:     2020/12/3 17:30
 */
public class Demo1 {
    
    
    static Object objectLock = new Object();
    public static void main(String[] args) {
    
    
        new Thread( () ->{
    
    
            synchronized (objectLock){
    
    
                System.out.println(Thread.currentThread().getName()+"开始执行");
                try {
    
    
                    objectLock.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            }
        },"A").start();

        new Thread( () ->{
    
    
            synchronized (objectLock){
    
    
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"发出通知..");
            }
        },"B").start();

    }
}

存在的问题:

①wait和notify不能脱离同步代码块或者同步方法,必须获取监视对象的锁;

②将notify放在wait方法前面,程序无法执行,无法唤醒;可以在线程A中添加sleep(3),让唤醒线程先执行。

//让唤醒线程先执行,就出现问题了,A线程一直处于等待状态,无法唤醒
try {
    
     TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
    
     }

总结:wait和notify方法必须要在同步块或者方法里面且成对出现使用,先wait后notify才ok。

Codition接口中的await和signal方法实现线程的等待和唤醒

package com.interview01;

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

/**
 * Copyright (C), 2018-2020
 * FileName: Demo1
 * Author:   kongfanyu
 * Date:     2020/12/3 17:30
 */
public class Demo1 {
    
    
    static Object objectLock = new Object();
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) {
    
    
        new Thread( () ->{
    
    
            lock.lock();
            try {
    
    
                System.out.println(Thread.currentThread().getName()+"开始执行...");
                try {
    
    
                    condition.await();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"结束执行...");
            }finally {
    
    
                lock.unlock();
            }
        },"A").start();

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

存在的问题:

①同样需要lock和unlock同步块的支持

②同样如果先唤醒再等待出现一直阻塞问题

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

(A)线程先要获得并持有锁,必须在锁块(synchronized或lock)中,

(B)必须要先等待后唤醒,线程才能被唤醒。

LockSupport类重点 park等待和unpark唤醒

  • 是什么

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

    • 官网解释

      public class LockSupport extends Object
      用于创建锁和其他同步类的基本线程阻塞原语。
      LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值10,默认是零。
      
      可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1.
      ------    
      这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
      
  • 主要方法

    • API: 查看文档

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

      //调用LockSupport.park()时
      public static void park(){
              
              
          UNSAFE.park(false,0L);
      }
      

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

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

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

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

  • 代码

    public static void main(String[] args) {
          
          
        Thread a = new Thread(() ->{
          
          
            //先让B线程执行,发出通知,A线程还是可以正常结束
            try {
          
           TimeUnit.SECONDS.sleep(3); } catch(Exception e){
          
          e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"开始执行....");
            LockSupport.park();//被阻塞...等待通知放行,它要通过需要有许可证
            System.out.println(Thread.currentThread().getName()+"结束执行....");
        },"A");
        a.start();
        //A线程先执行,阻塞,B线程3秒后执行,成功通知A线程。
        //try { TimeUnit.SECONDS.sleep(3); } catch (Exception e){ e.printStackTrace(); }
        Thread b = new Thread(() ->{
          
          
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"发出通知....");
        },"B");
        b.start();
    }
    

①正常+无锁块要求

②之前错误的先唤醒后等待LockSupport照样支持

  • 重点说明

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

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

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

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

    调用一次unpark就加1,由0变成1,

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

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

    形象的理解

    线程阻塞需要消耗凭证permit,这个凭证最多只有一个。

    当调用park方法时

    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
    • 如果无凭证,就必须阻塞等待凭证可用;

    而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

  • 面试题

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

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

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

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

猜你喜欢

转载自blog.csdn.net/kongfanyu/article/details/112726432