《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(二)

简介: 《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile

thread:线程ID

age:分代年龄

epoch:用于偏向重定向和偏向撤销

一个对象创建时:


如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的

thread、epoch、age 都为 0

偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟

如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、

age 都为 0,第一次用到 hashcode 时才会赋值


1) 测试延迟特性

@Slf4j(topic = "c.TestBiased")
public class TestBiased {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        log.debug(JolUtils.toPrintableSimple(dog));
        Thread.sleep(4000);//代码进行延时,可以通过VM参数禁用延时
        log.debug(JolUtils.toPrintableSimple(new Dog()));
    }
}
class Dog{
}

c97aa6f5be3255fee2cdaf08e253b85e.png

2) 测试偏向锁

//利用 jol 第三方工具来查看对象头信息(注意这里我使用工具类扩展了 jol 让它输出更为简洁)
//添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息
        synchronized (dog){
            log.debug(JolUtils.toPrintableSimple(dog));
        }
        log.debug(JolUtils.toPrintableSimple(dog));
    }
}

af8dffd25eaa467148eef8325f39d926.png


3)测试禁用

在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁。进行测试:

86d3fd8a25e833abbdd86340251f4aac.png

4) 测试 hashCode

acd346dad7bb802cf4faba23dd77f608.png

运行结果:

b326d6d018f65eb8481c82e04cd1cb05.png

思考:为啥调用hashCode()方法后,偏向锁会被禁用呢?

当调用hashCode后,需要Makeword存放31位的hashCode值,但是偏向锁状态下的threadID占54位,导致没有空间再存hashCode,所以就从Biased -> Normal。


4.2 撤销(偏向锁)

1. 调用对象 hashCode()

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,所以调用 hashCode 会导致偏向锁被撤销


轻量级锁会在锁记录中记录 hashCode

重量级锁会在 Monitor 中记录 hashCode

**注意:**在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking


2. 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

//轻量级锁应用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的。
public static void test02(){
    Dog dog = new Dog();
    new Thread(()->{
        log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息
        synchronized (dog){
            log.debug(JolUtils.toPrintableSimple(dog));
        }
        synchronized (TestBiased.class){//t1唤醒t2
            TestBiased.class.notify();
        }
        log.debug(JolUtils.toPrintableSimple(dog));
    },"t1").start();
    new Thread(()->{
        synchronized (TestBiased.class){
            try {
                TestBiased.class.wait();//t2先等t1执行完,再继续往下执行。(这样做是为了保证俩线程错开)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息
        synchronized (dog){
            log.debug(JolUtils.toPrintableSimple(dog));
        }
        synchronized (TestBiased.class){
            TestBiased.class.notify();
        }
        log.debug(JolUtils.toPrintableSimple(dog));
    },"t2").start();
}

4ee1530cd2a3f5a33265066fe922b52a.png

3.调用 wait/notify

wait和notify只有重量级锁有

//重量级锁应用场景:当多个线程交错执行,加锁时间未错开。对应着锁对象wait/notify的使用
public static void test03(){
    Dog dog = new Dog();
    Thread t1 = new Thread(() -> {
        log.debug(JolUtils.toPrintableSimple(dog));
        synchronized (dog) {
            log.debug(JolUtils.toPrintableSimple(dog));
            try {
                dog.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
          log.debug(JolUtils.toPrintableSimple(dog));
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (dog){
            log.debug("notify");
            dog.notify();
        }
        log.debug(JolUtils.toPrintableSimple(dog));
    }, "t2");
    t2.start();
}

9c9847b3ad15ca8e39f59e2e0c58c40d.png

4.3 批量重偏向

撤销:从可偏向到不可偏向。也就是一个线程对对象使用完了,不用了,另外一个线程再访问。批量冲偏向是对撤销的优化。

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象

的 Thread ID。

当撤销偏向锁阈值超过 20 次后(指撤销类的对象总次数),jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程


public static void test04() throws InterruptedException {
    Vector <Dog> list = new Vector <>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog dog = new Dog();
            list.add(dog);
            synchronized (dog) {
                log.debug(i + "\t" + JolUtils.toPrintableSimple(dog));
            }
        }
        synchronized (list) {
            list.notify();
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog dog = list.get(i);
            log.debug(i + "\t" + JolUtils.toPrintableSimple(dog));
            synchronized (dog) {
                log.debug(i + "\t" + JolUtils.toPrintableSimple(dog));
            }
            log.debug(i + "\t" + JolUtils.toPrintableSimple(dog));
        }
    }, "t2");
    t2.start();
}

运行结果:


d9861cad572ce5a3cafc264850f8c0b9.png


4.4 批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象

都会变为不可偏向的,新建的对象也是不可偏向的。

注意:这是40次是类级别的计数, 而不是对象级别的

static Thread t1,t2,t3;
public static void main(String[] args) throws InterruptedException {
    test05();
}
private static void test05() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 38;
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
            }
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
            synchronized (d) {
                log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
            }
            log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
            synchronized (d) {
                log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
            }
            log.debug(i + "\t" + JolUtils.toPrintableSimple(d));
        }
    }, "t3");
    t3.start();
    t3.join();
    log.debug(JolUtils.toPrintableSimple(new Dog()));
}

a4f42fb5884bd1acefc659afba465e4e.png

思考:加入t4线程后的对象状态是什么?


当loopNumber = 39时,锁对象(开始已经创建的这39个)状态都为Normal,不可偏向。由于撤销次数达到40,之后新创建的对象状态为Normal

当loopNumber < 39时,锁对象(开始已经创建的这loopNumber个)状态都为Normal,不可偏向。但是由于撤销次数 < 40,之后创建的新对象状态默认依旧为 偏向状态


5. 锁粗化

锁粗化就是,当多个方法重复调用锁synchronized ,比如在for 循环中,就可以相当于在synchronized中进行for循环,进行粗化

具体深入剖析待完善…

6. 锁消除

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    static int x = 0;
    @Benchmark
    public void a() throws Exception {
        x++;
    }
    @Benchmark
    public void b() throws Exception {
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }
}

ead84d1143e2011eb384df02fbe75817.png


**原因分析:**Java运行时有一个JIT即时编译器,会对字节码进行进一步优化。其中一个手段就是逃逸分析锁消除 — 看局部变量是否逃离作用范围,对于b(),其中的 o对象并不会逃离方法范围,给其加锁没有意义,JIT就会吧synchronized优化掉,相当于没有加锁。所以a()和b()性能非常相近。


其中默认 JIT逃逸分析的开关是打开的,可以通过-XX:-EliminateLocks 进行关闭


关闭后运行之:


c86e615edf6299d09e067fa43efd10f9.png


四、wait / notify原理


78bbc630a682acc3f50a0682d4df9929.png

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态


WAITING线程和BLOCKED线程的区别?


WAITING:已经获得了锁,但是条件不满足,释放锁后进入WaitSet队列;

BLOCKED:没有获得锁,进入EntryList中等待,状态是BLOCKED

==相同点:==都处于阻塞状态,不占用CPU时间片,将来调度时候也不会考虑。


WAITING线程和BLOCKED线程的唤醒条件?


WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争


BLOCKED 线程会在 Owner 线程释放锁时唤醒


相关文章
|
监控 安全
并发编程系列教程(06) - 多线程之间通讯(wait、notify、sleep、Lock锁、Condition)
并发编程系列教程(06) - 多线程之间通讯(wait、notify、sleep、Lock锁、Condition)
68 0
|
4月前
|
存储 SQL 安全
Java共享问题 、synchronized 线程安全分析、Monitor、wait/notify以及锁分类
Java共享问题 、synchronized 线程安全分析、Monitor、wait/notify以及锁分类
45 0
|
4月前
|
Java
Java中的线程通信:wait、notify与Condition详解
Java中的线程通信:wait、notify与Condition详解
|
Java C语言 C++
JUC--start线程
native:本地方法栈,c语言的函数,但凡调用的是这个接口,就是调用底层操作系统,第三方c语言的接口。 也就是说多线程和语言无关,是操作系统层面的东西。基本上每种语言都有自己的多线程实现方式。
|
Java
高并发编程-Wait Set 多线程的“休息室”
高并发编程-Wait Set 多线程的“休息室”
71 0
|
安全 Java C++
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
218 1
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
|
存储 安全 Java
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(一)
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(一)
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(四)
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(四)
|
缓存 索引
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(三)
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile
《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(三)
|
安全 Java API
《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)(一)
《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)
《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)(一)