-
多线程环境下的理论参考模型
-
为程序提供了极强的内存可见性保证
-
特性
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的操作执行顺序,不管程序是否同步
每个操作都必须原子执行且立刻对所有线程可见
- happens-before
MM中最核心的理论,保证内存可见性
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
-
理论
-
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
-
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
-
as-if-serial
所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变
synchronized
-
同步、重量级锁
-
原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
- 锁对象
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
-
实现机制
-
Java对象头
-
synchronized的锁就是保存在Java对象头中的
-
包括两部分数据
-
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳
-
Klass Pointer(类型指针)
-
monitor
-
Owner
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
-
锁优化
-
自旋锁
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
自旋字数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失
缺点:自旋次数无法确定
- 适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
- 锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据:变量逃逸
- 锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
- 轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
通过CAS来获取锁和释放锁
性能依据: 对于绝大部分的锁,在整个生命周期内都是不会存在竞争的
缺点:在多线程环境下,其运行效率比重量级锁还会慢
- 偏向锁
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁
volatile
- 特性
volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写
volatile原子性:volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如i++;
-
实现机制: 内存屏障
-
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
-
操作系统语义
-
主存、高速缓存(线程私有)缓存一致?
-
解决方案
通过在总线