volatile和synchronized的轻重关系
volatile不会引起线程上下文切换和调度
可见性的含义:
可见性处理的是线程之间的可见性
对象是线程之间的共享变量
以下分析基于intel处理器
1、volatile的定义与原理 volatile是排他锁的轻量替代品
一些术语
情景:cpuA正在执行threadA,threadA将要对一个变量name进行修改,在源代码中此变量由volatile修饰,编译后生成一条包含LOCK指令的语句。现在name变量本尊在内存中放着。
缓存行填充:cpuA将内存中的name变量取到缓存cacheA中,但尚未进行修改。缓存行填充:cpuB也将name变量取到自己的缓存cacheB中,但尚未进行运算。cpuA对name进行运算。(此时cpu注意到了LOCK指令,LOCK指令开始生效)写命中:cpuA将结果写回缓存cacheA。LOCK指令的效果:锁总线。在后来的cpu中,不使用锁总线而是锁缓存:
cacheA中的name立即写回到内存,覆盖内存中的name在MESI缓存一致性协议的作用下,发生了“锁缓存现象”:cacheB中的name数据所在的缓存行失效。cpuB要对name进行修改,发现缓存不命中,因为已经失效,于是重新从系统内存中读取数据name。
什么是MESI控制协议?
在多线程并发的环境中,一个cpu应当考虑:我的缓存、别人的缓存、内存:这三方的一致问题。MESI控制协议就是用来解决这个问题的。
基于嗅探技术,实现缓存行四种状态的转换。
如何理解linkedTransferQueue将头结点和为节点扩展到64字节的行为?
2.2、synchronized的原理与应用 2.2.1 java对象头对于一些以64字节作为缓存行长度的cpu来说,如果不扩展,那么可能头结点和尾节点进入同一缓存行,针对头结点锁缓存,就把本不需要锁的尾节点也给锁住了。
不能滥用,因为会带来性能损耗。更多的数据进入缓存行。
如何理解“Java中的每一个对象都可以作为锁”?
synchronized的三种使用形式:锁普通方法、锁静态方法、锁方法块:中,分别锁的是什么?
public synchronized void hello(){} //锁的是hello方法所在的对象public static synchronized void hello(){} //锁的是方法所在类synchronized(this){} //锁的是括号里面的对象
对象和monitor的关系是什么?#
monitorenter和monitorexit指令让cpu做什么?
java对象头结构:
首先:头的长度:数组对象3个字,非数组对象2个字。其次:头的内容:
数组对象:Mark Word ; Class metadata Address ; Array Length:标记、类地址、数组长度。非数组对象:只有标记、类地址。 最重要的一部分:标记
对象的五种锁状态,对应五种不同的Mark Word结构:无锁、轻量级锁、重量级锁、GC标记、偏向锁32位系统无锁状态对应的Mark Word结构:hashcode、分代年龄、0、01 2.2.2 锁的升级与对比
在1.6中有哪四种锁?
无锁、偏向锁、轻量级锁、重量级锁(级别递增)
将锁设计为只能升级不能降级的策略的目的?#
1 偏向锁基于以下情况来设计:在大多数情况下,锁不存在多线程竞争,而总是由同一线程多次获得。所以我们应该让同一线程多次获得锁的代价尽可能地小,据此设计出了偏向锁。
“偏向”的含义
让对象偏向一个线程:
具体实现:对象头中记录线程ID
“偏向”建立两步走:确认“偏向锁”标识,让锁“偏向”本线程
测试是否已经偏向本线程测试偏向锁标识
若没有设置:CAS设置若设置了:CAS进一步设置对象头,“偏向”本线程
偏向锁撤销的时机
时机:出现竞争,且全局安全点
全局安全点:所有的工作线程停止执行
这也决定了偏向锁的使用场景是:竞争很少。如果存在大量竞争,那么就会频繁出现偏向锁的撤销行为,而这个行为需要等待全局安全点,所以工作线程暂停,影响系统工作效率。
偏向锁撤销的流程
源码解析
if 对象不是偏向锁then 直接返回// 对象是偏向锁if 无偏向线程if 不允许重偏向then 设置无锁//有偏向线程,先判断存活if 如果当前线程是偏向线程else (当前线程不是偏向线程)判断偏向线程是否存活if 不存活if 允许重偏向then 设置mark word为匿名偏向else (不允许重偏向)设置无锁//有存活的偏向线程if 偏向所有者正在持有锁升级为轻量级锁,处理锁重入情况else (偏向所有者不正在持有锁)if 运行重偏向then 设置为匿名偏向else 设置无锁
总结:升级为轻量级锁的前提是:
1、对象是偏向锁
2、有存活的偏向线程
3、偏向所有者正在持有锁
2 轻量级锁注意:之前由于出现竞争,多个线程阻塞在全局安全点上,现在竞争解决后(不管是升级成为了轻量级锁还是恢复为无锁),之前阻塞的线程会继续执行。
加锁过程:
线程执行到了同步块
创建空间、复制Mark、Mark替换
若失败,自旋
以下参考
轻量级锁分为:
自旋锁
不会挂起线程,而是原地自旋等待,期望锁在较短时间能能被释放为自己所用。自旋过程会消耗cpu,所以不适用于等待时间过久的情况。可以通过设置一个固定的:“最大自选次数”,来避免过度占用cpu,默认这个数字是10。一旦超过这个数字,锁升级为重量级锁。 自适应自旋锁
记忆线程和锁之间的关系。如果一个线程曾经多次获得过一个锁,倾向于认为未来它有更大可能会再次获得这个锁,于是虚拟机延长该线程自旋次数。反之,如果一个线程过去很少获得该锁,虚拟机倾向于认为未来它也很难获取到该锁,有可能直接忽略这个线程的自旋过程,直接升级为重量级锁。
一旦升级为重量级锁,那么原来已经通过“占用轻量级锁”方式占用锁的线程就无法使用CAS操作来“解除轻量级锁”,因为锁已经变了,同一把钥匙打不开了。所以解锁失败后会采用解重量级锁的方式来解锁,也就是说:释放锁,唤醒等待的线程
轻重量级锁究竟不同在哪里?
3 总结两个升级过程:
参考
依赖于对象内部的monitor锁来实现
依赖于操作系统的Mutex(互斥) Lock来实现
涉及到线程的阻塞和唤醒,用户态和内核态的转变。
(以下操作基于intel处理器)
概念1:CPU流水线。将一条指令分为5-6步后,交给CPU中5-6个相应功能的电路单元来处理。于是可以在前一个指令尚未完成执行时,就开始下一条指令的执行。
最最基本的内存操作,比如从内存中读取或者写入一个字节,它的原子性由处理器保证。
而对于复杂的内存操作,处理器只提供机制,不保证自动实现原子性。
情景:
多处理器同时“读改写”一个共享变量,比如(同时执行i++),违反原子性。
这里“同时”的含义,多个CPU缓存了同一份变量。
实现:
处理器提供一个LOCK#信号。锁总线。
总线是处理器和内存沟通的桥梁。
总线锁住,则其他CPU无法访问总线,那么本CPU就可以独占共享内存
缓存锁的设计避免了总线锁的弊端
总线锁导致其他处理器无法操作其他内存地址的数据,而我们最初的需求,仅仅是不想让其他处理器操作指定内存地址的数据
实现:
首先,缓存锁依赖于CPU内部的三级高速缓存:L1 L2 L3。
参考
一个CPU将内存区域缓存到缓存中后,通过缓存一致性协议保证原子性。如果同时其他CPU已经对同一内存进行缓存,那么后者相应的缓存行被设置为无效。
3 Java实现原子操作 循环+CAS不使用缓存锁定的情况
1、不能缓存,或跨缓存行缓存。这是采用总线锁定。
2、处理器支持缓存锁定。
基于处理器的CMPXCHG指令。循环的原因是CAS可能会失败。失败是因为compare阶段失败。失败后循环直到成功。
值得注意的是,CMPXCHG是一个LOCK前缀的指令,会在内存区域加锁。这体现出CAS和volatile的联系。
CAS三大问题
ABA问题 解决方式:追加版本号。
1.5开始为Atomic包提供一个类AtomicStampedReference来解决这个问题。
compare的时候需要比较两个数据:Reference和Stamp。
其中Reference相当于数据本身,它可以辨别A和B的区别,而不能辨别第一个A和第二个A的区别。Stamp相当于版本号,它可以辨别第一个A和第二个A的区别。 自旋消耗的问题
自旋就会引起CPU开销。
书中提到了pause指令,这部分没搞懂。
在1.5之后,引入了AtomicReference来保证对象操作的原子性,可以将多个变量放在一个对象中进行CAS操作
JVM中的锁与CAS在JVM中,除了偏向锁,其他的锁的获取和释放都使用了循环CAS。