JUC多线程及高并发(三) 之 LockSupport & AQS

JUC多线程及高并发(三) 之 LockSupport & AQS

  • 一、可重入锁
    • (一)基本概念
    • (二)可重入锁种类
      • 1、隐式锁
      • 2、显示锁
  • 二、LockSupport
    • (一)基本概念
    • (二)3种线程等待唤醒的方法
      • 1、使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
      • 2 、使用JUC包中Condition接口中的await()方法让线程等待,使用signal()方法唤醒线程
      • 3、LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
    • (三)LockSupport 小结
  • 三、AQS——AbstractQueuedSychronizer
    • (一)基本概念
    • (二)为什么AQS是JUC内容中最重要的基石
    • (三)AQS 能干嘛
    • (四)AQS初步认识
    • (五)AQS的源码解析(通过ReentrantLock来理解AQS)

一、可重入锁

(一)基本概念

可重入锁又名递归锁,是指同一个线程 在外层方法获取锁的时候,再进入该线程的内层方法时,会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。(通俗来讲就是,洋葱最外面加了一把锁,里面每一层都是和最外面的锁一样,那么当我得到最外面的一把锁时,我就可以直接深入到洋葱的内心;如果最外面是一把,里面的每一层都不相同,那么我就卡在最外面了)

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

可重入锁的字面解释
可:可以。
重:再次。
入:进入——进入什么?进入同步域(即同步代码块/方法或显式锁 锁定的代码)
锁:同步锁

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

(二)可重入锁种类

1、隐式锁

即synchronized关键字使用的锁,默认是可重入锁(像自动档,由JVM控制)

1、同步代码块

public class ReEnterLockDemo {
     
    
    // synchronized 同步代码块可重入演示
    static Object objectLockA = new Object();

    public static void m1() {
     
        new Thread(() -> {
     
            synchronized (objectLockA) {
     
                System.out.println(Thread.currentThread().getName() + "\t" + "------外层调用");
                synchronized (objectLockA) {
     
                    System.out.println(Thread.currentThread().getName() + "\t" + "------中层调用");
                    synchronized (objectLockA) {
     
                        System.out.println(Thread.currentThread().getName() + "\t" + "------内层调用");
                    }
                }
            }
        }, "t1").start();

    }

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

程序运行结果:在同一个线程可以多次获取同一把锁
JUC多线程及高并发(三) 之 LockSupport & AQS_第1张图片
2、同步方法

public class ReEnterLockDemo {
     
    // synchronized 同步方法可重入演示

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

程序运行结果
JUC多线程及高并发(三) 之 LockSupport & AQS_第2张图片
synchronized 原理

1.在 IDEA 终端中进入包名所在的文件夹
在这里插入图片描述
2.使用 javap -c xxx.class 指令反编译字节码文件,可以看到有一对配对出现的 monitorenter 和 monitorexit 指令,一个对应于加锁,一个对应于解锁
JUC多线程及高并发(三) 之 LockSupport & AQS_第3张图片
为什么会多出来一个 monitorexit 指令呢?
如果同步代码块中出现Exception或者Error,则会调用 第二个monitorexit指令 来保证释放锁

小结 :有点像PV操作
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针。

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

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

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

2、显示锁

即Lock,也有ReentrantLock这样的可重入锁。(就像手动挡,要自己写)

代码示例一:可重入演示

public class ReEnterLockDemo {
     
    // ReentrantLock 可重入演示
    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(); //正常情况,加锁几次就要解锁几次
            }
        }, "t1").start();   
    }
}

程序运行结果:在同一个线程内部成功获取同一把锁
JUC多线程及高并发(三) 之 LockSupport & AQS_第4张图片
代码示例二:加锁几次就要解锁几次
错误示例:

public class ReEnterLockDemo {
     
    // ReentrantLock 可重入演示
    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();    //正常情况,加锁几次就要解锁几次,这里不解锁
            }
        }, "t1").start();

        new Thread(() -> {
     
            lock.lock();
            try {
     
                System.out.println("b thread----外层调用lock");
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                lock.unlock();
            }
        }, "t2").start();
    }
}

程序运行结果:执行到 t2 线程卡死,这是因为 t1 线程加了两次锁,但是之释放了一次锁,因此 t2 线程拿不到锁,程序无法正常结束
JUC多线程及高并发(三) 之 LockSupport & AQS_第5张图片
正确示例:

public class ReEnterLockDemo {
     
    // ReentrantLock 可重入演示
    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();    //正常情况,加锁几次就要解锁几次
            }
        }, "t1").start();

        new Thread(() -> {
     
            lock.lock();
            try {
     
                System.out.println("b thread----外层调用lock");
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                lock.unlock();
            }
        }, "t2").start();
    }

}

程序运行结果:t1 线程加了两次锁,释放了两次锁,t2 线程可以拿到锁,等到 t2 线程执行完后,程序结束
JUC多线程及高并发(三) 之 LockSupport & AQS_第6张图片

二、LockSupport

(一)基本概念

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

LockSupport中的park()是阻塞线程 unpark()的作用是解除阻塞线程,因此我们可以将其看作是线程等待唤醒机制(wait/notify)的加强版

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

(二)3种线程等待唤醒的方法

1、使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程

1、正常情况:实现线程的等待和唤醒

static Object objectLock = new Object();

private static void synchronizedWaitNotify() {
     
    new Thread(() -> {
     
        synchronized (objectLock) {
     
            System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
            try {
     
                objectLock.wait(); // 等待
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        }
    }, "A").start();

    new Thread(() -> {
     
        synchronized (objectLock) {
     
            objectLock.notify(); // 唤醒
            System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
        }
    }, "B").start();
}

程序运行结果:A 线程先执行,执行 objectLock.wait() 后被阻塞,B 线程在 A 线程之后执行 objectLock.notify() 将 A线程唤醒
JUC多线程及高并发(三) 之 LockSupport & AQS_第7张图片
2、异常情况一:不在 synchronized 关键字中使用 wait() 和 notify() 方法

static Object objectLock = new Object();

private static void synchronizedWaitNotify() {
     
    new Thread(() -> {
     
        //synchronized (objectLock) {
     
        System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
        try {
     
            objectLock.wait(); // 等待
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        //}
    }, "A").start();

    new Thread(() -> {
     
        //synchronized (objectLock) {
     
        objectLock.notify(); // 唤醒
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
        //}
    }, "B").start();
}

程序运行结果:不在 synchronized 关键字中使用 wait() 和 notify() 方法 ,将抛出 java.lang.IllegalMonitorStateException 异常
JUC多线程及高并发(三) 之 LockSupport & AQS_第8张图片
3、异常情况二:先 notify() 后 wait()

static Object objectLock = new Object();

private static void synchronizedWaitNotify() {
     
    new Thread(() -> {
     
        try {
     
            TimeUnit.SECONDS.sleep(3L);//目的是为了让B线程先走
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        synchronized (objectLock) {
     
            System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
            try {
     
                objectLock.wait(); // 等待
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        }
    }, "A").start();

    new Thread(() -> {
     
        synchronized (objectLock) {
     
            objectLock.notify(); // 唤醒
            System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
        }
    }, "B").start();
}

程序运行结果:B 线程先执行 objectLock.notify(),A 线程再执行 objectLock.wait(),这样 A 线程无法被唤醒
JUC多线程及高并发(三) 之 LockSupport & AQS_第9张图片
小结

  1. wait 和 notify方法 必须要在同步块(就是synchronized修饰的方法)里面成对出现使用
  2. 先wait 后 notify才OK

2 、使用JUC包中Condition接口中的await()方法让线程等待,使用signal()方法唤醒线程

1、正常情况:实现线程的等待和唤醒

static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

private static void lockAwaitSignal() {
     
    new Thread(() -> {
     
        lock.lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
            try {
     
                condition.await();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        } finally {
     
            lock.unlock();
        }
    }, "A").start();


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

程序运行结果:A 线程先执行,执行 condition.await() 后被阻塞,B 线程在 A 线程之后执行 condition.signal() 将 A线程唤醒
JUC多线程及高并发(三) 之 LockSupport & AQS_第10张图片
2、异常情况一:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法

static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

private static void lockAwaitSignal() {
     
    new Thread(() -> {
     
        //lock.lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
            try {
     
                condition.await();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        } finally {
     
            //lock.unlock();
        }
    }, "A").start();


    new Thread(() -> {
     
        //lock.lock();
        try {
     
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
        } finally {
     
            //lock.unlock();
        }
    }, "B").start();
}

程序运行结果:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法,将抛出 java.lang.IllegalMonitorStateException 异常
JUC多线程及高并发(三) 之 LockSupport & AQS_第11张图片
3、异常情况二:先 signal() 后 await()

static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

private static void lockAwaitSignal() {
     
    new Thread(() -> {
     
        try {
     
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        lock.lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
            try {
     
                condition.await();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
        } finally {
     
            lock.unlock();
        }
    }, "A").start();


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


程序运行结果:B 线程先执行 condition.signal(),A 线程再执行 condition.await(),这样 A 线程无法被唤醒
JUC多线程及高并发(三) 之 LockSupport & AQS_第12张图片
通过以上两种方式,我们可以知道:
传统的 synchronized 和 Lock 实现等待唤醒通知的约束

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

  2. 必须要先等待后唤醒,线程才能够被唤醒

3、LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

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

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

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

也就是说:每个线程都带有一个许可证,默认值为0(阻塞),如果你现在没有许可证,就不允许放行。由谁来发放这个许可证呢?就是我们的unpark方法。

阻塞

park() / park(Object blocker)

park() 方法的作用:阻塞当前线程 / 阻塞传入的具体线程

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

park() 方法通过 Unsafe 类实现

// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
     
    UNSAFE.park(false, 0L);
}

唤醒

unpark(Thread thread)

unpark() 方法的作用:唤醒处于阻断状态的指定线程

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

unpark() 方法通过 Unsafe 类实现

// Makes available the permit for the given thread
public static void unpark(Thread thread) {
     
    if (thread != null)
        UNSAFE.unpark(thread);
}

1、正常使用 LockSupport

private static void lockSupportParkUnpark() {
     
    Thread a = new Thread(() -> {
     
        System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
        LockSupport.park(); // 线程 A 阻塞
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
    }, "A");
    a.start();

    new Thread(() -> {
     
        LockSupport.unpark(a); // B 线程唤醒线程 A
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
    }, "B").start();
}

程序运行结果:A 线程先执行 LockSupport.park() 方法将通行证(permit)设置为 0,其实这并没有什么鸟用,因为 permit 初始值本来就为 0,然后 B 线程执行 LockSupport.unpark(a) 方法将 permit 设置为 1,此时 A 线程可以通行(完全不需要synchronized和lock)
JUC多线程及高并发(三) 之 LockSupport & AQS_第13张图片
2、先 unpark() 后 park()

private static void lockSupportParkUnpark() {
     
    Thread a = new Thread(() -> {
     
        try {
     
            TimeUnit.SECONDS.sleep(3L);//为了先执行线程B中的unpark
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis());
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
    }, "A");
    a.start();

    new Thread(() -> {
     
        LockSupport.unpark(a);
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
    }, "B").start();
}

程序运行结果:因为引入了通行证的概念,所以先唤醒(unpark())其实并不会有什么影响,从程序运行结果可以看出,A 线程执行 LockSupport.park() 时并没有被阻塞(尾号一样,说明LockSupport.park()形同虚设,没有起到阻塞的作用),也就是说,在LockSupport 中 unpark 可以在 park 前执行
JUC多线程及高并发(三) 之 LockSupport & AQS_第14张图片
3、异常情况:没有考虑到 permit 上限值为 1

private static void lockSupportParkUnpark() {
     
    Thread a = new Thread(() -> {
     
        try {
     
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis());
        LockSupport.park();
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
    }, "A");
    a.start();

    new Thread(() -> {
     
        LockSupport.unpark(a);
        LockSupport.unpark(a); //虽然unpark两次,但是只有一个证,一个证对应一个park,所以第二个park会被阻塞
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
    }, "B").start();
}

程序运行结果:由于 permit 的上限值为 1,所以执行两次 LockSupport.park() 操作将导致 A 线程阻塞
JUC多线程及高并发(三) 之 LockSupport & AQS_第15张图片
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个,累加无效。

(三)LockSupport 小结

以前的两种方式:

  • 以前的等待唤醒通知机制必须synchronized里面执行wait和notify
  • 在lock里面执行await和signal

这上面这两个都必须要持有锁才能干

LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点

LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死

LockSupport 相关问题

  1. 为什么可以先唤醒线程后阻塞线程?
    因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

  2. 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

三、AQS——AbstractQueuedSychronizer

(一)基本概念

前置知识
公平锁和非公平锁
可重入锁
LockSupport
自旋锁——CAS
数据结构之链表
设计模式之模板设计模式

概念
AbstractQueuedSynchronizer之AQS:抽象的队列同步器(抽象说明是抽象类,队列表示线程抢不到锁就要排队)
一般我们说的 AQS 指的是 java.util.concurrent.locks 包下的 AbstractQueuedSynchronizer
JUC多线程及高并发(三) 之 LockSupport & AQS_第16张图片

但其实共有三种抽象队列同步器:AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer 和 AbstractQueuedLongSynchronizer

AQS 是用来构建锁或者其它同步器组件的 重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
JUC多线程及高并发(三) 之 LockSupport & AQS_第17张图片
我们可以粗略的认为,AQS就是一个 state资源(int变量)+ CLH队列(FIFO队列)
CLH:Craig、Landin and Hagersten 队列,默认是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

(二)为什么AQS是JUC内容中最重要的基石

以下几个内容都和AQS有关

  1. ReentrantLock
    JUC多线程及高并发(三) 之 LockSupport & AQS_第18张图片
  2. CountDownLatch
    在这里插入图片描述
  3. Semaphore
    JUC多线程及高并发(三) 之 LockSupport & AQS_第19张图片

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

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

(三)AQS 能干嘛

加锁会导致阻塞

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

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

既然说到了排队等候机制,那么就一定 会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。(这里的共享资源相当于银行办理窗口,客户就相当于一个个线程,锁就是叫号的广播,叫号广播系统就是阻塞等待唤醒机制)。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。

(四)AQS初步认识

1.AQS如何管理thread
JUC多线程及高并发(三) 之 LockSupport & AQS_第20张图片
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

AQS虽然是管理Thread的,但是我们不是直接将Thread扔到AQS里面,而是先把thread放到node节点里面,再把node放到AQS里面。
JUC多线程及高并发(三) 之 LockSupport & AQS_第21张图片
Node 节点是啥
类似于 HashMap 的 Node 节点,JDK 用 static class Node implements Map.Entry { },来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread。

下面这张图只是我的猜测:(貌似猜对了)——node节点中还有一个waitstate变量,本图没表示
JUC多线程及高并发(三) 之 LockSupport & AQS_第22张图片
2.AQS内部体系架构
JUC多线程及高并发(三) 之 LockSupport & AQS_第23张图片
AQS的int变量
AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去
在这里插入图片描述
AQS的CLH队列
CLH队列(三个大牛的名字组成),为一个双向队列,类似于银行侯客区的等待顾客
JUC多线程及高并发(三) 之 LockSupport & AQS_第24张图片
综上:AQS= state + CLH变体的双向队列

AQS的内部类Node(Node类在AQS类内部)

  1. Node的int变量
    Node的等待状态waitState成员变量,表示当前结点在队列中的状态,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node

  2. Node此类的讲解
    通过 state 变量 + CLH双端( Node 队列实现 )
    JUC多线程及高并发(三) 之 LockSupport & AQS_第25张图片

(五)AQS的源码解析(通过ReentrantLock来理解AQS)

首先我们来看一张类图,由图可知:
ReentrantLock 实现了 Lock 接口,在 ReentrantLock 内部聚合了一个Sync类,Sync类实现了AbstractQueuedSynchronizer接口。
JUC多线程及高并发(三) 之 LockSupport & AQS_第26张图片
下面我们通过源码来理解这张图。

  1. Lock 接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类 完成线程访问控制的——比如ReentrantLock 类,如下图:
    JUC多线程及高并发(三) 之 LockSupport & AQS_第27张图片
  2. new ReentrantLock

ReentrantLock 默认是创建非公平锁,源码如下:

    public ReentrantLock() {
     
        sync = new NonfairSync();
    }

ReentrantLock有参构造,源码如下:(传入为true则是公平锁,传入false则为非公平锁)

public ReentrantLock(boolean fair) {
     
        sync = fair ? new FairSync() : new NonfairSync();
    }
  1. NonfairSync 和 FairSync 分别实现了获取锁的 非公平 与 公平策略
    JUC多线程及高并发(三) 之 LockSupport & AQS_第28张图片
    由上述的源码分析我们可以看出,ReentrantLock 最终还是使用 AQS 来实现的,并且根据参数来决定其内部是一个公平锁 还是 非公平锁,默认是非公平锁。

下面我们来看公平锁 和 非公平锁 的源码,可以得出:NoFairSync 和 FairSync 中 tryAcquire() 方法的区别,可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件: hasQueuedPredecessors()
hasQueuedPredecessors() 方法是公平锁加锁时判断等待队列中是否存在有效节点的方法
JUC多线程及高并发(三) 之 LockSupport & AQS_第29张图片
对比 公平锁 和 非公平锁 的tryAcqure()方法的实现代码, 其实差别就在于 非公平锁 获取锁时 比公平锁中少了一个判断 !hasQueuedPredecessors(),其他代码都长得一模一样。

hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

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

下面我们来走一遍流程:
JUC多线程及高并发(三) 之 LockSupport & AQS_第30张图片
而 acquire() 方法最终都会调用 tryAcquire() 方法


没错我还是很懵逼!那么就来举个栗子吧…
下面我们将带入一个银行办理业务的案例,来模拟我们的AQS如何进行 线程的管理 和 通知唤醒机制。
3个线程模拟来银行办理业务的顾客,银行柜台小姐姐只有一名

public class AQSDemo {
     
    public static void main(String[] args) {
     

        ReentrantLock lock = new ReentrantLock();
        
        // A线程就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
        new Thread(() -> {
     
            lock.lock();
            try {
     
                System.out.println("-----A thread come in");
                try {
     
                    TimeUnit.MINUTES.sleep(20); //办理个20min
                } 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();
    }
}

先来看看顾客A
顾客A是第一个来的,没有人和他抢
A的流程如下:
JUC多线程及高并发(三) 之 LockSupport & AQS_第31张图片
下面分析 final void lock() 方法
之前已经讲到过,new ReentrantLock() 不传参默认是非公平锁,调用 lock.lock() 方法最终都会执行 NonfairSync 重写后的 lock() 方法.
由于第一次执行 lock() 方法,state 变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state = = expected== 0,因此 CAS 成功,将 state 的值修改为 1JUC多线程及高并发(三) 之 LockSupport & AQS_第32张图片
再来看看顾客B
他先A的流程一样,先到窗口去看看有没有自己的位置,发现state=1,有人占用了
JUC多线程及高并发(三) 之 LockSupport & AQS_第33张图片
下面对 final boolean nonfairTryAcquire(int acquires) 进行分析
该方法继承了tryAcquire(arq)
JUC多线程及高并发(三) 之 LockSupport & AQS_第34张图片
没办法,b只能乖乖去排队了,源码流程如下:
JUC多线程及高并发(三) 之 LockSupport & AQS_第35张图片
源码流程图对应的动画如下:
JUC多线程及高并发(三) 之 LockSupport & AQS_第36张图片
JUC多线程及高并发(三) 之 LockSupport & AQS_第37张图片
最后来看看顾客C
他和B一样,不能占用窗口,只能乖乖去排队
JUC多线程及高并发(三) 之 LockSupport & AQS_第38张图片
JUC多线程及高并发(三) 之 LockSupport & AQS_第39张图片
看完了上面三个流程,总算不晕了(强烈建议不要光看图,自己去idea里面走一边源码流程,效果更佳!!)
注意
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个 有数据的节点,是从第二个节点开始的。

你以为这就结束了?并没有!还剩最后一个方法
final boolean acquireQueued(final Node node, int arg)

这个方法是干什么呢?前面的流程只是告诉线程B、C,你们两个要去排队了(那个队伍就是傀儡结点打头的),但是线程B、C还没有安安稳稳的坐在队伍里面呢!所以 acquireQueued 方法就是让 线程B、C 安安稳稳坐在队伍里(也就是把他们真正的阻塞)。
JUC多线程及高并发(三) 之 LockSupport & AQS_第40张图片
线程B、C总不能一直阻塞吧…所以需要被唤醒,怎么被唤醒呢?接着来看!
下面是线程A的操作,他通过 LockSupport.unpark 方法唤醒 线程B
JUC多线程及高并发(三) 之 LockSupport & AQS_第41张图片
这个线程B就可以杀回马枪占用窗口了
JUC多线程及高并发(三) 之 LockSupport & AQS_第42张图片
线程B占用窗口的时候,把自己的位置作为新的傀儡结点。
JUC多线程及高并发(三) 之 LockSupport & AQS_第43张图片
以上,我们通过 ReentrantLock 来分析 AQS源码,走的是非公平锁这条路。

你可能感兴趣的:(JUC)