AQS

2020-11-18  本文已影响0人  笨比乔治

什么是可重入锁?

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

优点:可一定程度避免死锁。

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

  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
  3. 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放

javap -c ***.class

在这里插入图片描述
防止异常要保证彻底释放锁和退出。
//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   内层....
        * */
    }
}
public class Test {

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

        new Thread(()->{
            lock.lock();
            lock.lock();
            try {
                System.out.println("============外层");
                lock.lock();
                try {
                    System.out.println("============内层");
                } finally {
                    lock.unlock();
                }

            } finally {
                lock.unlock();
//                lock.unlock();
            }
        },"t1").start();

        new Thread(()->{
            lock.lock();
            lock.lock();
            try {
                System.out.println("============外层");
                lock.lock();
                try {
                    System.out.println("============内层");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
                lock.unlock();
            }
        },"t1").start();
    }
}
image.png

②. 为什么要使用LockSupport

image.png
线程等待唤醒机制(wait/notify)的改良加强版 IMG_2131(20201027-152517).JPG

①. 3种让线程等待唤醒的方法
使用Object中的wait()方法让线程等待,使用Object中的notify方法唤醒线程
使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
②. Object类中wait( )和notify( )实现线程的等待唤醒
wait和notify方法必须要在同步块或同步方法里且成对出现使用。 wait和notify方法两个都去掉同步代码块后看运行效果出现异常情况:
Exception in thread “A” Exception in thread “B”
java.lang.IllegalMonitorStateException
先wait后notify才可以(如果先notify后wait会出现另一个线程一直处于等待状态)
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();
    }
}

④. LockSupport详解

4>. 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)
image.png
  1. 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
  2. static void unpark( )


    image.png
  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();
    }
}

⑥. 面试题目:

⑤. AbstractQueuedSynchronizer之AQS --抽象的队列同步器

①. AQS是什么?

image.png image.png image.png image.png image.png image.png

进一步理解锁和同步器的关系

锁 面向锁的使用者
同步器,面向锁的实现者


IMG_2132(20201027-165030).JPG

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

②. AQS内部体系架构

①. AQS内部架构图:


image.png

双向队列


image.png
image.png 在这里插入图片描述

③. ReentrantLock开始解读AQS

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


IMG_2134(20201027-173915).JPG

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

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 image.png

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


在这里插入图片描述

①. 代码展示:

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()

在这里插入图片描述 image.png

④tryAcquire(arg)

image.png

⑤. addWaiter(Node.EXCLUSIVE)

image.png 64B5B3D66087012B6F19A1207D177C04.png
for(;;)代表无限循环的意思。if里面的语句上面那个图👆
在这里插入图片描述

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

⑦. unlock( )获取permit

在这里插入图片描述 在这里插入图片描述
上一篇下一篇

猜你喜欢

热点阅读