jc_并发关键字
volatile
1)作用
如果一个字段被声明为 volatile
1)可见性:java内存模型会确保一个线程修改 这个变量的值 会对其他线程立即可见
2)有序性:禁止指令重排序
3)原子性:jmm内存模型规定,变量的load和store操作本身就是原子的
注意:
(1)基本类型:指的是值,修改值以后其他线程会立即可见
(2)引用类型:指的是地址,修改地址后其他线程会立即可见,修改这个变量的字段不会对其他线程立即可见
1 | //验证volatile引用类型 |
2)实现原理
(1)jvm层面-jmm内存模型语义
具体的,jvm用 内存屏障 来实现jmm内存模型规定的上述volatile语义(可见性、有序性)
内存屏障类型:
(1)LoadLoad屏障:在执行Load2前必须执行完Load1
(2)LoadStore屏障:在执行Store前必须执行完Load
(3)StoreStore屏障:在执行Store2前必须执行完Store1
(4)StoreLoad屏障:在执行Load前必须执行完Store
理论上,对于volatile变量,jvm插入的内存屏障顺序如下:
volatile读操作
LoadLoadBarrier,保证 volatile读 不能与 普通读/volatile读 重排序
LoadStoreBarrier,保证 volatile读 不能与 普通写/volatile写 重排序StoreStoreBarrier,保证 普通读或写/volatile写 不能与 volatile写 重排序
volatile写操作
StoreLoadBarrier,保证 volatile写 不能与 (普通读或写)/volatile读或写 重排序上面有几点要注意的是:
store前必须要load,这是jmm规定的
所以StoreStoreBarrier和StoreLoadBarrier会额外的保证了相关load操作
StoreLoadBarrier中多余的保证了 volatile写 不能与 普通读或写 重排序
但是不影响正确性,所以没关系
实际上,jvm插入的内存屏障和cpu有关
(1)X86架构处理器,只会对 写-读 做重排序,不会对 读-读、读-写 和 写-写 这3种操作做重排序
所以,jvm对X86处理器,只会插入 StoreLoad屏障
(2)单核处理器没有必要插入内存屏障,针对单核处理器,jvm没有使用内存屏障
//例如orderAccess_linux_x86.inline.hpp,图片来源于https://zhuanlan.zhihu.com/p/133851347
(2)硬件层面
X86架构处理器使用lock前缀的cpu指令实现内存屏障(volatile)
上图的源码中有体现,1)作用 中验证volatile引用类型也有体现
具体查看 jc_原子操作篇
3)volatile缓存行填充
伪共享问题:
缓存系统中是以缓存行(cache line)为单位存储的
一般的cpu缓存行为64kb
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行
就会无意中影响彼此的性能
解决办法:
使用一些无意义的变量填充整个对象
使 有意义数据+无意义数据 的内存占用大于64b
即保证一个缓存行中只能有有意义数据
1 | //jdk7,Disruptor框架中的应用 |
1 | //jdk8 |
synchronized
1)作用
synchronized用来实现同步,java中的每一个对象都可以作为锁
2种使用形式:
1)同步方法
(1)对于普通同步方法,锁是当前实例对象
(2)对于静态同步方法,锁是当前类的Class对象
2)同步代码块
(2)锁是synchonized括号里配置的对象
2)实现原理
(1)jvm层面
(1)对于同步方法,jvm采用ACC_SYNCHRONIZED标记符来实现同步
(2)对于同步代码块,jvm采用monitorenter、monitorexit两个字节码指令来实现同步上述两者都是通过 Monitor(管程或监视器) 实现的,每个对象都有自己的Monitor
同步方法是隐式的,当调用同步方法时,会先检查是否有ACC_SYNCHRONIZED,如果有则需要先获得监视器锁,方法执行完后要释放监视器锁
同步代码块则显示的直接使用的两个字节码指令
1 | public static void main(String[] args) throws Exception { |
(1.1)对象头markword
对象结构详情请查看 jvm_内存管理篇-对象内存布局(64位虚拟机)
64位jvm中java对象不同状态下的markword
lock + biased_lock 共同确定了上述5种不同的状态,状态不同,整个markword标识的含义不同
1)无锁状态:
lock:锁标记,2位,值为01
biased_lock:偏向锁标记,1位,值为0,代表非偏向锁状态
age:垃圾回收java对象年龄,4位,垃圾回收时对象每次在survivor区复制一次,年龄增加1次,具体查看 jvm_垃圾回收篇
identity_hashcode:对象hashCode,31位,方法System.identityHashCode()计算得到的,延迟计算,计算后写入到这部分。其他状态下markword没有空间存储此值,hashcode移动到每个对象的监视器Monitor中
unused:未使用到的2)偏向锁状态:
biased_lock:偏向锁标记,1位,值为1,代表偏向锁状态
thread:持有偏向锁的线程ID,54位
epoch:用于批量重偏向/撤销,2位,较为复杂3)轻量级锁状态:
ptr_to_lock_record:指向栈中锁记录的指针,62位4)重量级锁状态:
ptr_to_heavyweight_monitor:指向对象监视器Monitor的指针,62位
部分内容参考 https://blog.csdn.net/SCDN_CP/article/details/86491792
(1.2)锁的升级过程
jdk1.6之后为了减少使用同步锁的性能消耗,优化了synchonized,使用synchonized时锁会有一个升级的过程
无锁状态(匿名偏向状态) -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态
内容来自这位大佬 https://github.com/farmerjohngit/myblog ,防止丢失,内容已缓存
概述
偏向锁
轻量级锁
重量级锁
是非公平锁
当一个线程想获取锁时,
先试图插队,
如果占用锁的线程释放了锁,
下一个线程还没来得及拿锁,
那么当前线程就可以直接获得锁;
如果锁正在被其它线程占用,
则排队,
排队的时候就不能再试图获得锁了,
只能等到前面所有线程都执行完才能获得锁
(2)硬件层面
1)更改markword值是使用的cas操作(Atomic::cmpxchg)
2)重量级锁使用的是POSIX中mutex锁
https://github.com/farmerjohngit/myblog/issues/7
final
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
(防止对象引用逃逸)
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

赞赏是不耍流氓的鼓励