前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CAS、ABA问题、锁升级

CAS、ABA问题、锁升级

原创
作者头像
孑小白
修改2021-05-13 15:34:15
4170
修改2021-05-13 15:34:15
举报

CAS

全称:Compare and swap或者Compare and exchange

翻译:比较和交换

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

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

代码语言:txt
复制
int N = 0;

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

代码语言:txt
复制
int E = N;
int V = E++;

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

代码语言:txt
复制
if(E==N){
    N = V;
}

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

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

代码语言:txt
复制
AtomicInteger i = new AtomicInteger();
i.incrementAndGet();

进入incrementAndGet()

代码语言:txt
复制
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}

进入getAndAddInt()

代码语言:txt
复制
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对象布局:首先注入依赖

代码语言:txt
复制
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

测试代码

代码语言:txt
复制
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

查看对象内存布局

代码语言:txt
复制
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压缩情况

代码语言:txt
复制
java -XX:+PrintCommandLineFlags -version

输出:

代码语言:txt
复制
-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();在内存中占多少字节?

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

锁升级

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

代码语言:txt
复制
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添加信息

锁升级过程:

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

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

详细讲解锁升级过程:

无状态锁new Object的时候。

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

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

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

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CAS
  • ABA
  • JOL
  • 锁升级
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档