CAS、synchronized和volatile

CAS、synchronized和volatile

十月 29, 2020

一.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为例。

1
2
3
4
5
6
7
8
9
10
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值。

1
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

锁消除

1
2
3
4
5
public String test(String a,String b) {
StringBuffer sb = new StringBuffer();
sb.append(a).append(b);
return sb.toString();
}

上面这段代码在实际运行时,JVM会检测出加锁对象都在一个方法里面,所以为了避免反复加锁,JVM不会加上锁。

锁粗化

1
2
3
4
5
6
7
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有两个作用:

  • 保证线程可见性
  • 防止指令重排序

防止指令重排序的实现

JVM的规范是用内存屏障。
在读之前加入一个读屏障,在写之前加入一个写屏障,屏障会保证所有之前写操作都已经结束,并且更新过的数据可见,因为屏障会把数据刷新到缓存,所有线程读到的都是最新的。

单例模式中有这段代码:

1
2
3
4
5
6
7
if (singleton != null) {
synchronized(Singleton.class) {
if (singleton != null) {
singleton = new Singleton();
}
}
}

对singleton加上volatile是防止第二次检查时候,new指令会发生重排序。
new在CPU看来分成三步

  • 1、分配空间
  • 2、初始化
  • 3、把引用赋值

下面是个人理解,网上没有找到说的很明白的:
     指令重排序会导致先3后1,另一个线程在第一层检查就会直接return,这样单例得到的对象没有被初始化,就会出现问题。
有了内存屏障,会保证第一层检查之前,singleton的所有写操作全部结束,也就是说初始化singleton的线程会把那三步执行完。这样,即使指令重排序了,也能保证得到的结果是最新的。