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了,其实这时候锁降级也就没什么意义了