《Java 虚拟机》 happens-before 与锁优化

简介: 《Java 虚拟机》 happens-before 与锁优化

🚀1. happens-before

🎁 从 JDK 5 开始,Java 使用新的 JSR-133 内存模型,该内存模型使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。------《Java 并发编程的艺术》


🎉happens-before 的八个规则如下:


🎈程序次序规则:一个线程内,按照代码顺序,书写在前的操作 happens-before 书写在后的操作

🎈管程锁定规则:对一个锁的解锁操作,happens-before 随后对这个锁的加锁操作(“后面”指时间上面的先后顺序)

🎈volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后面对这个变量的读操作(“后面”指时间上面的先后顺序)

🎈线程启动规则:Thread 对象的 start() 方法 happens-before 此线程的每一个动作

🎈线程终止规则:线程中所有操作都 happens-before 对此线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行

🎈线程中断规则:对线程 interrupt() 方法的调用 happens-before 被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupt() 方法检测到是否有中断发生

🎈对象终结规则:一个对象的初始化完成 happens-before 它的 finalize() 方法的开始

🎈传递性:如果操作 A happens-before 操作 B,操作 B happens-before 操作 C,那么操作 A happens-before 操作 C

🎉注意:两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前!


例如,线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

public class Demo4_3 {
    volatile static int x;
    public static void main(String[] args) {
        new Thread(() -> {
            x = 10;
        }, "t1").start();
        new Thread(() -> {
            System.out.println(x);
        }, "t2").start();
    }
}

例如,线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

public class Demo4_4 {
    static int x;
    static Object m = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (m) {
                x = 10;
            }
        }, "t1").start();
        new Thread(()->{
            synchronized (m) {
                System.out.println(x);
            }
        }, "t2").start();
    }
}

例如,线程开始前对变量的写,对该线程开始后对该变量的读可见

public class Demo4_5 {
    static int x;
    public static void main(String[] args) {
        x = 10;
        new Thread(()->{
            System.out.println(x);
        }, "t1").start();
    }
}

例如,线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join() 等待它结束)

public class Demo4_6 {
    static int x;
    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(()->{
            x = 10;
        }, "t1");
        t1.start();
        t1.join();
        System.out.println(x);
    }
}

例如,线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)

public class Demo4_7 {
    static int x;
    public static void main(String[] args) {
        Thread t2 = new Thread(()->{
            while (true) {
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();
        new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 10;
            t2.interrupt();
        }, "t1").start();
        while (!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
}

🚀2. 锁优化

🎉Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为标记位 、 线程锁记录指针 、 重量级锁指针 、 线程ID 等内容。

🚁2.1 轻量级锁

🎉如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有多线程竞争),那么可以使用轻量级锁来优化,这就好比:


🎉学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。

如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁,进入重量级锁的流程。


🎉而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来假设有两个方法同步块,利用同一个对象加锁。

static Object obj = new Object();
    public static void method1() {
        synchronized (obj) {
            // 同步块A
            method2();
        }
    }
    public static void method2() {
        synchronized (obj) {
            // 同步块B
        }
    }

🎉每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

线程1 对象 Mark Word 线程2
访问同步块 A,把 Mark Word 复制到线程 1 的锁记录 01(无锁)
CAS 修改 Mark Word 为线程 1 锁记录地址 01(无锁)
成功(加锁) 00(轻量锁)线程 1 锁记录地址
执行同步块 A 00(轻量锁)线程 1 锁记录地址
访问同步块 B,把 Mark Word 复制到线程 1 的锁记录 00(轻量锁)线程 1 锁记录地址
失败(发现是自己的锁) 00(轻量锁)线程 1 锁记录地址
锁重入 00(轻量锁)线程1锁记录地址
执行同步块B 00(轻量锁)线程 1 锁记录地址
同步块 B 执行完毕 00(轻量锁)线程 1 锁记录地址
同步块 A 执行完毕 00(轻量锁)线程 1 锁记录地址
成功(解锁) 01(无锁)
01(无锁) 访问同步块 A,把 Mark Word 复制到线程2的锁记录
01(无锁) CAS 修改 Mark Word 为线程 2 锁记录地址
00(轻量锁)线程 2 锁记录地址 成功(加锁)

🚁2.2 锁膨胀

🎉如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object();
    public static void method1() {
        synchronized (obj) {
            // 同步块
            method2();
        }
    }
线程1 对象 Mark Word 线程2
访问同步块,把 Mark Word 复制到线程 1 的锁记录 01(无锁)
CAS 修改 Mark Word 为线程 1 锁记录地址 01(无锁)
成功(加锁) 00(轻量锁)线程 1 锁记录地址
执行同步块 00(轻量锁)线程 1 锁记录地址
访问同步块 00(轻量锁)线程 1 锁记录地址 访问同步块,把 Mark Word 复制到线程 2
执行同步块 00(轻量锁)线程 1 锁记录地址 CAS 修改 Mark Word 为线程 2 锁记录地址
执行同步块 00(轻量锁)线程 1 锁记录地址 失败(发现别人已经占用了锁)
执行同步块 00(轻量锁)线程 1 锁记录地址 CAS 修改 Mark Word 为重量锁
执行同步块 10(重量锁)重量锁指针 阻塞中
执行完毕 10(重量锁)重量锁指针 阻塞中
失败(解锁) 10(重量锁)重量锁指针 阻塞中
释放重量锁,唤起阻塞线程竞争 01(无锁) 阻塞中
10(重量锁) 竞争重量锁
10(重量锁) 成功(加锁)

🚁2.3 重量锁

🎉重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。


🎉在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。


🎈自旋等待本身虽然避免了线程切换的开销,但是也会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 才能发挥自旋优势。好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)

🎈Java 7 之后不能控制是否开启自旋功能

🎉自旋重试成功的情况

线程1(cpu1上) 对象 Mark 线程2(cpu2上)
10(重量锁)
访问同步块,获取 monitor 10(重量锁)重量锁指针
成功(加锁) 10(重量锁)重量锁指针
执行同步块 10(重量锁)重量锁指针
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
10(重量锁)重量锁指针 成功(加锁)
10(重量锁)重量锁指针 执行同步块

🎉自旋重试失败的情况

线程1(cpu1上) 对象 Mark 线程2(cpu2上)
10(重量锁)
访问同步块,获取 monitor 10(重量锁)重量锁指针
成功(加锁) 10(重量锁)重量锁指针
执行同步块 10(重量锁)重量锁指针
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞

🚁2.4 偏向锁

🎉轻量级锁在没有竞争时(只有自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,进而提高了效率。


🎈撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)

🎈访问对象的 hashCode 也会撤销偏向锁

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

🎈撤销偏向和重偏向都是批量进行的,以类为单位

🎈如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的

🎈可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

假设有两个方法同步块,利用同一个对象加锁

static Object obj = new Object();
    public static void method1() {
        synchronized (obj) {
            // 同步块A
            method2();
        }
    }
    public static void method2() {
        synchronized (obj) {
            // 同步块B
        }
    }
线程1 对象Mark Word
访问同步块 A,检查 Mark Word 中是否有线程 ID 101(无锁可偏向)
尝试加偏向锁 101(无锁可偏向)对象 hashCode
成功 101(无锁可偏向)线程ID
执行同步块 A 101(无锁可偏向)线程ID
访问同步块 B,检查 Mark Word 中是否有线程 ID 101(无锁可偏向)线程ID
是自己的线程 ID,锁是自己的,无需做更多操作 101(无锁可偏向)线程ID
执行同步块 B 101(无锁可偏向)线程ID
执行完毕 101(无锁可偏向)对象 hashCode
hashCode

🚁2.5 其他优化

🪂2.5.1 减少上锁时间

🎉同步代码块中尽量短。

🪂2.5.2 降低锁的粒度

🎉将一个锁拆分为多个锁提高并发度,例如:


🎈ConcurrentHashMap

🎈LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值

🎈LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高

🪂2.5.3 锁粗化

🎉多次循环进入同步块不如同步块内多次循环,另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

new StringBuffer().append("a").append("b").append("c");

🪂2.5.4 锁消除

🎉JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。

🪂2.5.5. 读写分离

  • 🎈CopyOnWriteArrayList
  • 🎈ConyOnWriteSet
相关文章
|
3月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
127 4
|
3月前
|
数据采集 搜索推荐 Java
Java 大视界 -- Java 大数据在智能教育虚拟学习环境构建与用户体验优化中的应用(221)
本文探讨 Java 大数据在智能教育虚拟学习环境中的应用,涵盖多源数据采集、个性化推荐、实时互动优化等核心技术,结合实际案例分析其在提升学习体验与教学质量中的成效,并展望未来发展方向与技术挑战。
|
5月前
|
资源调度 安全 Java
Java 大数据在智能教育在线实验室设备管理与实验资源优化配置中的应用实践
本文探讨Java大数据技术在智能教育在线实验室设备管理与资源优化中的应用。通过统一接入异构设备、构建四层实时处理管道及安全防护双体系,显著提升设备利用率与实验效率。某“双一流”高校实践显示,设备利用率从41%升至89%,等待时间缩短78%。该方案降低管理成本,为教育数字化转型提供技术支持。
156 1
|
1月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
121 8
|
2月前
|
Java Spring
如何优化Java异步任务的性能?
本文介绍了Java中四种异步任务实现方式:基础Thread、线程池、CompletableFuture及虚拟线程。涵盖多场景代码示例,展示从简单异步到复杂流程编排的演进,适用于不同版本与业务需求,助你掌握高效并发编程实践。(239字)
226 6
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
7月前
|
自然语言处理 Java 关系型数据库
Java|小数据量场景的模糊搜索体验优化
在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。
170 0
|
5月前
|
消息中间件 机器学习/深度学习 Java
java 最新技术驱动的智能教育在线实验室设备管理与实验资源优化实操指南
这是一份基于最新技术的智能教育在线实验室设备管理与实验资源优化的实操指南,涵盖系统搭建、核心功能实现及优化策略。采用Flink实时处理、Kafka消息队列、Elasticsearch搜索分析和Redis缓存等技术栈,结合强化学习动态优化资源调度。指南详细描述了开发环境准备、基础组件部署、数据采集与处理、模型训练、API服务集成及性能调优步骤,支持高并发设备接入与低延迟处理,满足教育机构数字化转型需求。代码已提供下载链接,助力快速构建智能化实验室管理系统。
165 44
|
3月前
|
存储 人工智能 算法
Java 大视界 -- Java 大数据在智能医疗影像数据压缩与传输优化中的技术应用(227)
本文探讨 Java 大数据在智能医疗影像压缩与传输中的关键技术应用,分析其如何解决医疗影像数据存储、传输与压缩三大难题,并结合实际案例展示技术落地效果。
|
3月前
|
机器学习/深度学习 算法 Java
Java 大视界 -- Java 大数据机器学习模型在生物信息学基因功能预测中的优化与应用(223)
本文探讨了Java大数据与机器学习模型在生物信息学中基因功能预测的优化与应用。通过高效的数据处理能力和智能算法,提升基因功能预测的准确性与效率,助力医学与农业发展。
下一篇
oss云网关配置