一.CAS
CAS
native方法。修改一个值,当前为0,现在要加一,在写回的时候,判断该变量是否还是0,。
ABA问题
CAS会有一个问题,如果该变量还是0,不一定代表他没有被人修改过。比如另一个线程对他加2,然后又被减2,虽然最后还是0,但是他不是最开始的那个0.
解决办法:
- 可以加一个bool表示是否修改过
- 加一个版本号
CAS底层汇编实现
用 AtomicInteger 一步一步查到最后。
java native代码->虚拟机jvm的c++代码->linux的汇编代码
lock cmpxchg
lock的意思是后面的指令不能被其他CPU打断,这样就能保证在cmp的时候值是不变的。
jdk1.8 Unsafe类
Unsafe类里面有很多CAS方法,以AtomicInteger用到的getAndAddInt为例。
public final int getAndAddInt(Object var1, long var2, int var4) {
// 三个参数分别是内存的值,期望值,也就是当前读到的值,修改之后的值
//只有前两个值相等,才会把第三个值更新到内存
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
注意单纯的compareAndSwapInt不会循环,只会compare一次,并返回bool值。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
二.Synchronized
对象内存布局
对象内存=markword(锁信息 8字节)+class pointer(类型指针,表示属于哪个类 4字节)+instance data(实例数据)+padding(对齐 需要为8的整数倍)。所以new一个什么都没有的Object,new Object()是16个字节(8(markword) + 4(class pointer) + 4(padding))。
注意64位虚拟机开启压缩之后class pointer是4字节,不压缩是8字节
示例
以user{int id,string name}为例
markword 8
classpointer 4(压缩)
int 4
string 4(压缩)
padding 4
所以一共是24字节
markword+class pointer是对象头。
synchronized加锁过程
首先synchronized是锁住对象,不是锁住代码块。
synchronized的锁自动升级过程:
new Obj(无锁)->偏向锁->轻量级锁(自旋锁,自适应自旋)->重量级锁
锁降级的过程:
来源:知乎
重量级锁的降级发生于STW阶段,降级对象就是那些仅仅能被VMThread访问而没有其他JavaThread访问的对象。也就是说只有GC的时候才降级,那对象都没了,降级不降级也没有意义了。
汇编语言实现方式:lock cmpxchg
锁消除
public String test(String a,String b) {
StringBuffer sb = new StringBuffer();
sb.append(a).append(b);
return sb.toString();
}
上面这段代码在实际运行时,JVM会检测出加锁对象都在一个方法里面,所以为了避免反复加锁,JVM不会加上锁。
锁粗化
public String test(String a,String b) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
return sb.toString();
}
JVM检测到这样的代码,循环的反复加锁解锁,JVM会把加锁操作放到循环体外,这样只用加一次锁。
三.volatile
volatile有两个作用:
- 保证线程可见性(内存屏障,工作内存强制写入内存)
- 防止指令重排序
单例模式中有这段代码:
if (singleton != null) {
synchronized(Singleton.class) {
if (singleton != null) {
singleton = new Singleton();
}
}
}
对singleton加上volatile是防止第二次检查时候,new指令会发生重排序。
new在编译器编译时会把这个语句分成三步
- 1、分配空间
- 2、赋值
- 3、初始化
编译器编译之后的指令就不再是123,而是132 。此时如果线程1执行完13之后把时间片交给线程2,线程2取这个对象时发现已经有地址了,不是null,就会取走,但是这个对象并没有赋值,之后线程2就会出错。