四、高效并发
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(轻量级锁)
网友评论