《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 线程释放锁时唤醒


相关文章
|
7月前
|
存储 缓存 Java
JUC并发—3.volatile和synchronized原理
本文介绍了volatile关键字的使用、主内存和CPU的缓存模型、CPU高速缓存的数据不一致问题、总线锁和缓存锁及MESI缓存一致性协议、Java的内存模型JMM、JMM如何处理并发中的原子性可见性有序性、volatile如何保证可见性、volatile为什么无法保证原子性、volatile如何保证有序性、volatile的原理(Lock前缀指令 + 内存屏障)、双重检查单例模式的volatile优化、基于volatile优化微服务的优雅关闭机制、优化微服务存活状态检查机制等 14.i++的多线程安全问题演示 1
|
存储 索引
【数据结构】HashSet的底层数据结构
【数据结构】HashSet的底层数据结构
566 2
|
消息中间件 监控 供应链
深度剖析 RocketMQ 事务消息!
本文深入探讨了 RocketMQ 的事务消息原理及其应用场景。通过详细的源码分析,阐述了事务消息的基本流程,包括准备阶段、提交阶段及补偿机制。文章还提供了示例代码,帮助读者更好地理解整个过程。此外,还讨论了事务消息的优缺点、适用场景及注意事项,如确保本地事务的幂等性、合理设置超时时间等。尽管事务消息增加了系统复杂性,但在需要保证消息一致性的场景中,它仍是一种高效的解决方案。
1079 2
|
负载均衡 安全 Java
微服务 Gateway 使用详解
网关(Gateway)是连接不同网络并进行数据转发的关键组件。在互联网中,路由器常作为默认网关;在现代操作系统中,网关指本地网络上转发数据包的设备。Spring Cloud Gateway是一款基于Spring Framework的API网关,具备反向代理、高性能、负载均衡、安全控制、限流熔断、日志监控等功能。通过简单配置即可实现请求路由和转发,适用于微服务架构中的集中控制、解耦客户端与服务、自动服务发现等场景,提升系统安全性与可扩展性。
1627 4
|
安全 物联网 API
API的科普
在当今这个数字化时代,信息如同血液般在无数个系统、应用和设备之间流淌,而这一切高效、无缝的交互背后,离不开一个至关重要的技术组件——API(Application Programming Interface,应用程序编程接口)。API作为数字世界的桥梁,不仅连接了不同的软件系统,还推动了数据共享、业务自动化以及创新服务的不断涌现。本文将深入探讨API的定义、作用、发展历程、关键技术、应用场景以及未来趋势,旨在揭示API在数字化转型中的核心价值和无限潜力。
1761 1
|
Java Spring
|
XML Java 数据格式
Spring中BeanFactory和FactoryBean详解
Spring中BeanFactory和FactoryBean详解
733 1
|
测试技术 程序员
W模型和瀑布模型与“V”模式开发模型有何异同?
W模型和瀑布模型与“V”模式开发模型有何异同?
668 1
|
机器学习/深度学习 人工智能 自然语言处理
OpenAI 推出 GPT-4o,免费向所有人提供GPT-4级别的AI ,可以实时对音频、视觉和文本进行推理,附使用详细指南
GPT-4o不仅提供与GPT-4同等程度的模型能力,推理速度还更快,还能提供同时理解文本、图像、音频等内容的多模态能力,无论你是付费用户,还是免费用户,都能通过它体验GPT-4了
870 1
|
算法 Java C++
《经典图论算法》迪杰斯特拉算法(Dijkstra)
这个是求最短路径的迪杰斯特拉算法,另外我还写了50多种《经典图论算法》,每种都使用C++和Java两种语言实现,熟练掌握之后无论是参加蓝桥杯,信奥赛,还是其他比赛,或者是面试,都能轻松应对。