美文网首页
深入理解Java虚拟机读书笔记(四)

深入理解Java虚拟机读书笔记(四)

作者: Corey1874 | 来源:发表于2022-05-03 11:33 被阅读0次

    四、高效并发

    1. Java内存模型与线程

    1.1 概述

    计算机大部分时间都花磁盘I/O,网络通讯,数据库访问,CPU大部分时间都在等待其他资源的状态,因此需要同时处理多个任务

    另一个并发应用场景,就是服务端同时对多个客户端提供服务

    1.2 硬件效率与一致性

    CPU运算速度比访问内存速度快得多,因此加上了高速缓存,将数据复制到缓存中,CPU从缓存中读取数据高速运算,运算结束后把结果从缓存同步到主内存。这样可以解决处理器和内存速度矛盾,但是会引入缓存一致性问题。多处理器访问同一主内存区域,各自缓存可能不一致。因此各处理器访问内存需要遵循协议:MSI MESI MOSI Synapse Firefly Dragon Protocol等

    处理器对代码可能进行乱序执行优化。相应的,虚拟机即时编译器也有类似的指令重排序优化

    1.3 Java内存模型

    主内存与工作内存

    所有共享的变量在主内存中,每个线程有自己的工作内存,线程对变量的操作都必须在工作内存中进行,而不能直接操作主内存,线程间变量传递需要主内存

    内存间交互操作

    八个指令:

    • lock:作用于主内存,把主内存变量标识为线程独占

    • unlock:作用于主内存,把主内存变量解锁,解锁后其他线程才能锁定

    • read:作用于主内存,把变量的值从主内存传到工作内存

    • load:作用于工作内存,把主内存得到的值,放到工作内存变量副本

    • use:作用于工作内存,虚拟机遇到需要使用变量的字节码指令时会执行。把变量的值传给执行引擎

    • assign:作用于工作内存,虚拟机遇到给变量赋值的字节码指令时会执行。把从执行引擎接收到的值赋值给工作内存的变量

    • store:作用于工作内存,把工作内存中的变量的值传到主内存

    • write:作用于主内存,把从工作变量中得到的值放到主内存变量中

    注意:

    • read、load与store、write必须按先后顺序执行,但是中间可以穿插其他操作

    • read、load与store、write必须成对出现,即不允许从一边读了但另一边不接受

    • assign了就一定要同步回主内存

    • 没有assign过不允许同步回主内存

    • 不允许工作内存中直接使用未初始化(assign/load)的变量,use store操作前,必须执行过了assign和load

    • lock可以执行多次,但需要解锁同样次数

    • 执行lock前,会清空工作内存中此变量的值。执行引擎使用此变量前,需要先assign或load重新初始化

    • 没有lock不允许unlock

    • unlock前必须把变量同步回主内存,即执行store、write

    对于volatile型变量规则

    volatile可以保证变量对所有线程可见,但并不是绝对线程安全,多写场景下仍然有并发问题,因为写的操作不是原子的。volatile适合一写多读场景

    volatile另一个语义是禁止指令重排序优化

    volatile的读效率与正常变量差不多,写效率慢一写,因为需要插入内存屏障

    对于long和double型变量规则

    虽然虚拟机规范中允许把64位数据分为两次32位操作,但具体实现时,仍然会把64位数据作为原子操作

    原子性、可见性、有序性

    原子性:6个基本操作是原子性的,如果不能满足需要,可以用lock、unlock指令,对应字节码指令monitorenter、monitorexit

    可见性:一个线程修改了变量的值,其他线程能够立即知道修改的值。volatile、final、synchronized三个关键字都能够保证变量可见性。被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程就能看见final字段的值。synchronized的可见性指的是,unlock操作前,必须把变量从工作内存同步到主内存

    有序性:volatile可以禁止指令重排序,synchronized是可以保证同一时刻只有一个线程访问

    先行发生原则

    天然有先后顺序,无需进行同步控制

    1.4 Java与线程

    一对一,映射到轻量级进程

    调度方法:抢占式调度

    线程状态转换

    2. 线程安全与锁优化

    2.1 线程安全

    多个线程访问一个对象,不用考虑线程调度和交替执行,不用额外同步,调用这个对象都可以获取到正确的结果

    Java中的线程安全

    • 不可变:如final修饰,比如String、Number的部分子类(Integer、Long等),注意,AtomicInteger等不属于

    • 绝对线程安全:没有

    • 相对线程安全:线程安全的容器,比如Vector、CurrentHashMap,Collections中的SynchronizedCollection()方法包装的集合

    • 线程兼容:调用端进行同步,可以保证多线程安全使用,比如HashMap

    • 线程对立:很少,已废弃

    实现方法

    • 互斥同步:即加锁,synchronized、Lock

    • 非阻塞同步:无锁,CAS

    • 无同步方案:线程本地存储,如ThreadLocal

    2.2 锁优化

    自旋锁与自适应自旋

    忙循环,CPU不让出执行权

    自旋一定次数还没有获取到锁,可以省略自旋

    锁消除

    基于逃逸分析,如果不会被其他线程访问到,可以消除同步措施

    锁粗化

    循环加锁扩展到外部只加一次锁

    轻量级锁

    对象头包含两部分信息:1.对象自身运行时数据,比如哈希码,分代年龄,64位OS中长度是64bit;2.方法区类型数据指针,如果是数组,则还有数组长度

    [图片上传失败...(image-79696f-1651548783059)]

    线程中的LockRecord空间,可以存储对象markword的拷贝,即Displaced Mark Word。加锁的时候,CAS地更新对象markword为指向LockRecord的指针,如果更新成功,则标志位置为了00,加锁成功,没有则加锁失败。解锁过程就是把指针更新回Displaced Mark Word,如果更新成功则解锁成功,失败则说明有其他线程尝试获取过锁,要释放锁的时候,唤醒被挂起的线程。如果有2个以上线程竞争,则升级为重量级锁,标志位为10.

    偏向锁

    对一个无锁对象,把线程id记录到markword中,再有一个线程来获取锁,标志位恢复01(无锁)或00(轻量级锁)

    相关文章

      网友评论

          本文标题:深入理解Java虚拟机读书笔记(四)

          本文链接:https://www.haomeiwen.com/subject/ceibyrtx.html