1. volatile的原理
volatile,轻量级的synchronized,保证的是一个线程修改后,另一个线程立马可见的可见性,不会引起上下文切换。
底层实现
volatile Singleton instance = new Singleton()
JIT编译器生成的指令代码如下:
0x01a3deld:
mov $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0, (%esp);
有volatile修饰的变量进行写操作时会多出第二行汇编代码,lock指令在多核处理器下产生两个结果:
- 将当前处理器缓存的数据写回系统内存;
- 写回内存操作使其它CPU里缓存了该内存地址的数据无效,导致重新从系统内存读取;
以上操作导致了该变量的修改在不同线程之间是可见的。
2. synchronized的原理与应用
synchronized是重量级锁,随着Java1.6进行优化以后,引入了偏向锁、轻量级锁,以及锁的存储结构和升级过程。
synchronized锁有三种形式:
- 普通同步方法,锁是当前实例;
- 静态同步方法,锁是当前Class对象;
- 同步方法块,锁是括号里配置的对象;
synchronized是由JVM实现的,代码块同步是通过编译后在代码开始和结束(包括异常)位置分别使用monitorenter和monitorexit指令来实现的,同步方法则是方法属性里加入同步标记来实现。任何对象都有一个monitor与之关联。
1)Java对象头
synchronized使用的锁是存在Java对象头里的,对象为数组类型,则虚拟机使用3个字宽(32位虚拟机一个字宽为32位,即4个字节),非数组类型使用2个字宽存储对象头。
长度 | 内容 | 说明 |
---|---|---|
32/64 bit | Mark Word | 对象的hashCode、锁信息 |
32/64 bit | Class Metadata Address | 对象类型数据的地址 |
32/64 bit | Array Length | 数组长度(数组类型专有) |
MarkWord的存储结构:
锁状态 | 25bit | 4bit | 1bit 是否偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象HashCode | 对象分代年龄 | 0 | 01 |
2)锁升级
Java 1.6为了减少锁获取和释放带来的性能损耗,引入了“偏向锁”和“轻量级锁”,级别从低到高:无锁->偏向锁->轻量级锁->重量及锁。锁可以升级,但不能降级。
- . 偏向锁
一个线程访问同步块时,会在对象头和栈帧的锁记录里存储当前线程ID,以后该线程在进入和退出时不需要CAS来加解锁,只需简单测试MarkWord里是否有偏向锁指向自己。
- 轻量级锁
加锁:执行同步块之前,JVM先在当前线程栈帧中创“锁记录”空间,并将MarkWord复制到“锁记录”中,然后尝试使用CAS将对象头中MarkWord替换为指向“锁记录”的指针,成功则获取锁,失败则使用“自旋”来获取锁。
解锁:使用原子操作将“锁记录”替换回对象头,成功则表示没有竞争,失败表示存在竞争,锁膨胀为重量级锁。
3. 原子操作实现原理
1)处理器如何实现原子锁
- 总线锁定
使用处理器提供的Lock#信号,一个处理器输出此信号时,其他处理器请求将被阻塞。锁定开销较大。 - 缓存锁定
内存区域被缓存在处理器的缓存行中,Lock期间被锁定,执行回写内存时(缓存一致性会阻止同时修改由两个处理器缓存的内存区),会使其他缓存了该内存区的数据无效,导致重新读取内存。
2) Java中原子操作
实现方式:
- 锁:如之前内容所述,获取锁的线程才能操作指定共享的内存区域;
- 循环CAS:使用了处理器提供的CMPXCHG指令;
循环CAS存在的问题:
1. ABA问题,解决方法:版本号机制,每次操作版本号+1;
2. 循环时间长,开销大;
3. 只能保证一个变量的原子操作;