CAS原理-ABA问题-锁升级过程

CAS

全称:Compare and swap或者Compare and exchange

翻译:比较和交换

在多线程访问同一资源的时候,做到不加锁依然可以实现同步,原理是这样的:

1.首先读取该值假如该值N初始状态为0,那么读取该值以后保存到自定义的变量E中

int N = 0;

2.计算结果假如做递增操作,那么使E++并且保存为新的变量V中

int E = N;
int V = E++;

3.比较两个指把E和被访问的那个N进行比较,如果和之前是一样的,说明别的线程没有修改该指,那么就把V的值赋值给N

if(E==N){
    N = V;
}

4假如说被读取的那个值N,在我需要写入的时候改变了,那么就重复之前的操作,把现在的N读取进去,赋值给E,运算以后得到V,判断E和N还是否相等,如果相对,那么就让V赋值给N,这样的方式来进行修改。

我们不妨来追溯源码:AtomicInteger可实现不加锁递增

AtomicInteger i = new AtomicInteger();
i.incrementAndGet();

进入incrementAndGet()

public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}

进入getAndAddInt()

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
       v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

在while循环当中有个条件:weakCompareAndSetInt其实就是使用了CAS操作,在深入就是native级别了,跟踪到底层硬件级别,最终会发现CompareAndExchange对应的汇编指令:cmpxchg,但是对于单核CPU而言直接比较交换即可,对于多核CPU而言则需要加上lock指令,于是最终汇编指令为:lock cmpxchg

lock我们发现一个端倪:假如我们CAS在进行比较的和过程当中,这个值被修改了呢?其实原理是这样的:当一个CPU在执行改值操作的时候,如果是多核CPU,那么会执行lock指令,表示:当前CPU在执行的时候,不允许别的CPU打断执行

ABA

基于SAC的ABA问题:其他线程修改数次后的值和原本的一样

通俗点理解:你的女朋友和你分手之后又经历了其他的男人,之后有和你复合了,这就叫做ABA。

问题来了:虽然回到了原本的状态,但是也经历中间状态,假如中间状态产生了一定的影响,那么其他线程在访问的时候必须要感知到这个被修改过的状态

解决办法:给原本的值增加一个版本号,每次修改时,不仅仅访问比较这个值,还需要比较版本号,在JDK当中也有使用boolean类型来描述该值是否被修改过

在JDK中有个类:AtomicStampedReference,叫做加标签的参考值

JOL(Java对象布局)

首先注入依赖


org.openjdk.jol
jol-core
0.9

测试代码

Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

查看对象内存布局

OFFSET  SIZE TYPE DESCRIPTION  VALUE
0     4   (object header)   01 00 00 00 (00000001 00000000 00000000 00000000)(1)
4     4   (object header)   00 00 00 00 (00000000 00000000 00000000 00000000)(0)
8     4   (object header)   e5 01 00 20 (11100101 00000001 00000000 00100000)(536871397)
12      4   (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

当我们new一个对象的时候,他的结构:

  • markword:对象头,比如synchronized锁定的头信息
  • class pointer:类型指针:用来指向该对象所属的类型
  • instance data:实例化数据
  • padding:对齐,当字节数不足8时,自动补位,为了运算的流畅度

输入命令查看一下JVM对ClassPointer压缩情况

java -XX:+PrintCommandLineFlags -version

输出:

-XX:InitialHeapSize=123691904
   -XX:MaxHeapSize=1979070464
   -XX:+PrintCommandLineFlags
   -XX:+UseCompressedClassPointer
   -XX:+UseCompressedOops
   -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

可以看到HotSpot是64位的,那么按理说他的类指针也应该是64位,也就是8个字节,由于“-XX:+UseCompressedClassPointer”默认开启了指针压缩,所以默认情况下位4个字节。

所以markword8个字节,classpointer占4个字节,整个对象头占12个字节,由于Object的实例化无数据所以是0,最后对齐,所以填补4个空字节,一共16字节

Object o = new Object();在内存中占多少字节?

1.'new Object()'在Java默认开启CompressedClassPointer时,应该是占16个字节
那么前面的'Object o'引用占4个字节,总共20个字节。
2.如果没有开启CompressedClassPointer时:那么markword:8字节、class pointer:4字节、
没有实例数据:0字节、对齐:无需对齐,因为本身就16字节可以被8整除,那么就是16字节。

锁升级

首先执行下面这一段代码:

Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

可以发现被加锁后的对象数据Value:

加锁前

01 00 00 00 (00000001 00000000 00000000 00000000) (1)

枷锁后

28 f6 23 03 (00101000 11110110 00100011 00000011) (52688424)

可以发现当使用synchronized对对象加锁的时候,其实是对markword添加信息

锁升级过程:

1.刚刚new出对象开始时未上锁
2.第一次对其加锁被称之为:偏向锁
3.接下来升级为轻量级锁:无锁或者自旋锁
4.最终升级为:重量级锁

理解自旋锁和无所自旋锁:假如有一哥们在蹲坑,你在旁边转圈等待,轮到你了就立马上!你可以叫他自旋锁,也相当于不是锁,所以叫做:无锁

详细讲解锁升级过程:

无状态锁new Object的时候。

偏向锁很早之前jdk版本,是直接向操作系统OS申请重量级锁,然后直接上锁,但是这种效率不高,于是后来改进为偏向锁,而偏向锁类似于给需要访问的对象贴标签,表明这是属于自己的位置

自旋锁当偏向锁无法满足需求的时候:只要被访问的资源处于竞争状态时,自动升级为自旋锁,多线程同时并发访问同一个为资源,此时每条线程在自己的线程栈当中生成一个Lock Record对象,并且开始以CAS的自旋方式去抢占被访问的资源,该资源会记录轻量级锁的指针,也就是说会不断的比较被抢占资源的值是否与自己的指针是否相等,如果相等,那么就修改该指针

重量级锁当自旋锁长期处于自旋状态,太过于消耗CPU资源,于是升级为重量级锁,重量级锁是必须要由操作系统匹配的(锁的资源有限),重量级锁相当于队列使得线程处于wait状态,等待该线程执行时,系统会通知该线程执行,所以不消耗CPU资源。

注意:synchronized本身是一个不公平的锁,多线程时,谁先执行不一定

锁降级在GC的时候会发生,但是都已经GC了,其实这时候锁降级也就没什么意义了

你可能感兴趣的:(CAS原理-ABA问题-锁升级过程)