文章目录
1 CAS
1.1 CAS概念
CAS,即Compare And Swap或Compare And Exchange,意为比较更新。如图:
当一个线程要对某个变量a做更新操作时,先读取a的当前值E,通过计算得到结果值V,将V重新赋值给a之前,先要比较a的当前新值N是否和E相等。若不相等,意味着a的值已经被其他线程修改过,则不能将V赋值给a,需要重新读取a的当前值并计算,循环往复,直到N和E相等时,才能将结果值V赋值给变量a。
1.2 CAS的ABA问题
在上述过程中,当前新值N和前一次读取的值E相等时,对变量a进行更新操作。但是,变量a的值可能已经被另外一个线程修改过并且又改回了原来的值,a的值经历了从A变成B又变成A的过程,而主线程并不知道这个过程,这就是著名的ABA问题。解决这个问题的方法,只需要在a中加入一个版本号,每次修改a的值之后,版本号加1。那么,在修改a的值的时候,只需比较a的版本号是否被更改就可以了。
1.3 CAS在JDK中的应用
JDK中使用了大量的CAS操作,比如原子类AtomicInteger、jdk1.8中的ConcurrentHashMap和AQS等。AtomicInteger.incrementAndGet()方法:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
通过以上AtomicInteger.incrementAndGet()的源码可知,其底层实现是CAS操作。compareAndSwapInt方法是一个本地方法,跟踪其C++源码可知,其最终实现是一条汇编指令:lock cmpxchg。
2 Java对象内存布局
2.1 Java对象内存布局
普通Java对象在内存中由对象头、实例数据、8字节对齐位三部分组成,而数组是由对象头、数组长度、实例数据、8字节对齐位四部分组成。其中对象头又包括Markword和ClassPointer两部分,Markword占据8个字节,ClassPointer类型指针在默认情况下(开启指针压缩-XX:+UseCompressedClassPointers)占4个字节。类型指针指向的是该对象所属的类,而Markword将在下面的内容中详细介绍。
2.2 工具JOL(Java Object Layout)
首先,需要在pom.xml文件中引入依赖:
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
运行以下代码:
public static void main(String[] args) {
Object o = new Object();
String layout = ClassLayout.parseInstance(o).toPrintable();
System.out.println(layout);
}
输出:
java.lang.Object object internals:
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
以上结果证实了2.1中所述的关于Java对象内存布局的结论,而当给对象o加锁的时候,对象会有什么变化呢?
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
String layout = ClassLayout.parseInstance(o).toPrintable();
System.out.println(layout);
}
}
输出:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 40 f7 53 03 (01000000 11110111 01010011 00000011) (55834432)
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
从上面可以看出,给对象加锁之后,对象头中前8个字节也就是Markword发生了变化。所以,对一个对象进行加锁,实际上就是改变了Markword里面的内容。
2.3 Markword
Markword到底包含了什么信息呢?查看HotSpot源码markOop.hpp:
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
3 synchronized锁升级
- 新建一个对象o,如果JVM还未开启偏向锁,则该对象为无锁状态的普通对象,如果JVM已开启偏向锁,则该对象为匿名偏向锁状态。
- 当一个线程对o进行加锁时,如果o是一个普通对象,则o会进入轻量级锁的状态;如果o是一个匿名偏向对象,则该对象会进入具体的偏向锁状态,其Markword中将会存在线程的指针。
- 当o处于偏向锁的状态下,如果有轻度的竞争出现,JVM会撤销o的偏向锁,升级为轻量级锁状态。多个线程通过CAS操作将自身的LockRecord设置到Markword中,设置成功的线程表示获得锁。如果有重度竞争,JVM会撤销o的偏向锁,直接升级为重量级锁。
- 当o处于轻量级锁的状态下,如果竞争愈发激烈,对象会从轻量级锁升级为重量级锁。轻量级锁升级为重量级锁的条件为,当线程自旋次数超过10或者自旋线程数超过CPU核数的一半。在JDK1.6之后,加入了自适应自旋,由JVM来控制自旋次数。