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

四、高效并发

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(轻量级锁)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容