多线程-synchronized底层实现

前言

synchronized在JDK1.6的优化之后,性能上有了较大的提升。

synchronized优化的内容是锁升级机制。

  • 偏向锁、自旋锁、重量级锁
  • 锁升级条件、过程
  • 底层实现

各类锁的含义

JDK1.6之后,synchronized的锁机制按照开销从小到大排列,依次可分为:偏向锁、自旋锁(轻量级锁)和重量级锁。

重量级锁

  • JDK1.6之前,synchronized只有一种锁机制,即重量级锁。
  • 获取锁失败的线程会进入阻塞(Blocked)状态,目标锁的释放会唤醒这些线程。
  • 线程的阻塞和唤醒都需要通过系统调用,由操作系统来完成,其中涉及到用户态与内核态的切换,开销很大。这也是通常来说重量级锁性能较低的原因。

自旋锁

  • 通过CAS操作来获取锁,如果CAS操作成功,则获取到锁,如果失败,则继续自旋尝试获取锁。
  • 获取锁失败的线程不会阻塞,仍然继续自旋尝试获取锁。
  • 自旋锁的加锁和解锁不涉及线程状态的变化,CAS操作也不要系统调用,因此,相比于重量级锁,节省了系统调用的开销。

偏向锁

  • 偏向锁只存在于仅有一个线程请求锁的情况下,一旦超过一个线程请求锁,则锁升级为自旋锁。
  • 偏向锁仅在该线程第一次获取锁时,通过CAS操作在锁上记录线程标识,之后再尝试获取锁时,如果发现锁上的线程标识和本线程相符,直接进入同步代码块,无需再通过CAS操作加锁。退出同步代码块也无需释放锁操作。
  • 引入偏向锁的原因是:Hotspot作者经研究发现,大多数情况下,锁不仅不存在多线程竞争,还总是由同一个线程多次获得,因而通过引入偏向锁,能够让这种情况下的加锁解锁操作开销进一步降低。

思考

  1. 偏向锁一定比自旋锁高效吗?

    不一定,偏向锁是针对无竞争且每次是由同一个线程获取锁的情况的优化。如果明确知道存在竞争,那么偏向锁的设置和撤销就是无意义的消耗,不如直接升级为自旋锁。

  2. 自旋锁一定比重量级锁高效吗?

    不一定,重量级锁的开销在于加锁和解锁过程的系统调用开销,对于重量级锁,加锁失败的线程进入Blocked状态,不再占用CPU资源。

    而对于自旋锁,加锁失败的线程一直处于Runnable状态,会一直自旋再次尝试获取锁。

    因此,如果竞争很激烈(即有很多线程尝试获取锁),或同步代码段执行的时间非常长(即加锁失败的线程需要长时间自旋等待),那么加锁失败线程的自旋就会消耗大量CPU资源,也就得不偿失了。

上面的两个问题就引出了锁升级的概念。

锁升级

synchronized锁升级过程如下图所示:

锁升级

  1. 偏向锁功能在JVM中有两个相关参数可以配置:

    • -XX:-UseBiasedLocking=ture :是否使用偏向锁,默认开启。

    • -XX:BiasedLockingStartupDelay=4000 :偏向锁功能开启延时,默认4000,即JVM启动4秒后,才开启偏向锁功能。

    • 之所以默认配置了4秒的开启延时,是因为,在JVM启动过程中,明确知晓存在很多竞争,为了避免偏向锁撤销和锁升级带来的无意义的消耗,因此延时一段时间,等JVM启动完成后,才开启偏向锁功能。
  2. 一个锁对象new出来之后,如果未开启偏向锁功能,那么这就是一个普通对象,而如果开启了偏向锁功能,此时锁上没有任何一个线程标识,称之为匿名偏向。

  3. 普通对象加锁后,即成为自旋锁;匿名偏向锁加锁后,成为偏向锁。

  4. 偏向锁不可重偏向,一旦有第二个线程尝试对偏向锁加锁,偏向锁便撤销,并升级为自旋锁。

  5. 如果竞争加剧,自旋锁升级为重量级锁。

  6. 何为竞争加剧:

    • 某个线程自旋次数超过指定值,可通过 -XX:PreBlcokSpin=10 配置,默认值为10。
    • 或者自旋等待该锁的进程超过指定值,一般为CPU核数的一半。
    • 自适应自旋(Adapative Self Spinning):两个条件的阈值由JVM自己动态控制。

底层实现

了解上面锁的类型和升级过程之后,我们可能还会有疑问:

为什么synchronized可以将任何对象作为锁?

怎么区分一个锁对象是何种类型的锁?

偏向锁的线程标识存的是什么?存在哪儿?

synchronized可重入是如何实现的?

……

这就需要了解Synchronized更底层的实现了。

java对象内存布局

首先了解一下HotSpot实现中,一个java对象(Object)在内存中的布局。

内存布局

如下表所示,一个对象在内存中通常可以分为以下4部分:mark word、class pointer、instance data和内存对齐。

内容 大小 备注
mark word 8字节
class pointer 4字节 指向对象所属的类
instance data 对象中的成员所占的空间
内存对齐 0~7字节 8字节对齐

其中mark word中的内容和锁紧密相关。

mark word

mark word占用8字节空间,其中的内容随着对象状态的不同而变化,如下表所示:

mark word

可以看到根据一个对象的最后3位,就能判断出此对象当前是普通对象、正在被垃圾回收还是处于某种锁状态。

可重入

可重入锁是指:同一个线程可以多次对同一把锁执行加锁操作,而不会引起死锁。

synchronized锁必须是可重入的,否则某些java特性就会引发死锁,比如:父类中有个synchronized方法,子类重写了该方法,也是synchronized方法,子类的方法中又通过super调用了父类的该方法,相当于对同一把锁加锁了两次,如果synchronized不支持重入,那么这种情况就会引发死锁。

那么,synchronized是如何实现可重入的呢?

关键在于记录两个东西:

  1. 线程的标识,否则无法判断是否是同一个线程加锁;
  2. 加锁的次数,因为需要对应次数的解锁才能释放锁。

接下来我们就来看看不同的锁分别是如何实现的:

  • 偏向锁

    • mark word中就记录了线程指针
    • 不需要记录加锁次数,因为偏向锁不存在释放锁的概念,一旦有其他线程尝试拿锁,就会升级为自旋锁。
  • 自旋锁

    • 通过Lock Record来实现重入。

      Lock Record在Hot Spot源码中的class名称是BasicLock,其中只有一个成员 _displaced_header,用于备份该对象加锁前的mark word。

      Lock Record 加锁的对象 成对以BasicObjectLock的结构存储在线程栈中,每加一次锁,都会向栈中存入一个Lock Record。

    • 从mark word的内存布局可以看到,当锁标志是00时,mark word中记录了Lock Record的指针,再次加锁时会通过 is_lock_owned 函数判断对象mark word中的Lock Record指针是否在本线程的栈中,如果是,则表示锁是本线程持有的。

    • 自旋锁并没有通过记录加锁的次数来实现解锁和加锁次数的对应。自旋锁通过以下方法实现:

      第一次加锁时,会把对象的mark word中的原内容备份到Lock Record的displaced header中,并通过CAS的方式修改mark word,将Lock Record的地址存入mark word中。

      之后,当同一个线程再次加锁时,会生成新的Lock Record放入栈中,但是displaced header中存的是NULL,也不会再修改mark word。

      解锁时,如果从栈中弹出的Lock Record中的displaced header中存的是NULL,就表示锁还没有解完,锁仍然还由本线程持有。

      直到弹出的Lock Record中,displaced header中内容不为NULL,将displaced header中的内容还原到对象的mark word中,锁成功释放。

  • 重量级锁

    • 重量级锁通过ObjectMonitor实现。
    • ObjectMonitor中记录了锁的持有线程_owner和重入的次数_recursions
    • 此外,ObjectMonitor还备份了对象加锁前的mark word,即_header

HashCode

一旦某个对象计算过identity hashcode,该值就会存储在对象的mark word中,下次再需要获取该对象的hashcode时,就直接返回mark word中记录的hashcode。

但是从markword的布局图中,我们看到,对象被加锁后,mark word中就不再存有hashcode了,难道加锁的对象就没有hashcode了吗?

答案显然是否定的。

  • 对于自旋锁和重量级锁,hashcode可以从LockRecod或者ObjectMonitor中的displaced header中读出。
  • 而对于偏向锁,实际上偏向锁和hashcode不能共存,一旦对处于偏向锁状态的对象计算hashcode,如果偏向锁未被持有,那么会变为普通对象,如果偏向锁被持有了,则膨胀为自旋锁或重量级锁。

实验验证

想要通过实验验证锁的升级过程,就需要能够直观的看到一个对象在内存中的原始数值,通过 ClassLayout 类可以将一个java对象在内存中的原始数据以方便观察的格式打印出来。使用方法也很简单:

  1. 引入mvn依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
    </dependency>
  2. 在源码中输出:

    1
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

普通对象 加锁 -> 自旋锁

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class T01_Synchronized {

public static void main(String[] args) throws InterruptedException {

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

synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
}
}

分别打印了一个Object实例在加锁前后的内存布局。

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 38 f6 e6 02 (00111000 11110110 11100110 00000010) (48690744)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 38 f6 e6 02 (00111000 11110110 11100110 00000010) (48690744)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们看到打印出的lock对象的内存布局,每行展示了4字节数据,VALUE一栏分别以16进制、2进制和10进制三种方式打印出了值。

前两行共8字节,就是mark word的值,打印是按低字节在前的顺序打印的,因此第一个字节的bit0~1就是锁标志。

可以看到加锁前,锁标志是 0 1,偏向锁位是 0,其他位都是0,即这是一个普通对象,之所以不是匿名偏向锁,是因为程序刚运行时,偏向锁功能还未启用。

加锁后,锁标志变成了0 0,即自旋锁,且其他位已经有数据,其中存储的是Lock Record的地址。

再次加锁,程序仍然能继续执行,证明synchronized是可重入的,且mark word中存储的Lock Record地址仍然是初次加锁的Lock Record地址,并没有变。

匿名偏向 加锁 -> 偏向锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class T01_Synchronized {

public static void main(String[] args) throws InterruptedException {

Thread.sleep(5000);

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

synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 a6 02 (00000101 00111000 10100110 00000010) (44447749)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

这次首先sleep 5秒,待偏向锁功能启用后,再new一个Object,打印加锁前后,该对象的内存布局。

可见,加锁前,锁标志为:0 1,偏向锁位为:1,markword中其余位均为0,为匿名偏向状态。

加锁后,mark word 中记录了线程指针(JavaThread *)。

偏向锁与hashcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class T01_Synchronized {

public static void main(String[] args) throws InterruptedException {

Thread.sleep(5000);

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

Integer i = lock.hashCode();
System.out.println("HashCode: " + Integer.toHexString(i));
System.out.println(ClassLayout.parseInstance(lock).toPrintable());

synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}

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

i = lock2.hashCode();
System.out.println("HashCode: " + Integer.toHexString(i));
System.out.println(ClassLayout.parseInstance(lock2).toPrintable());
}
}
}

首先产生一把匿名偏向锁,接着对该对象计算hashcode,观察前后对象内存布局;

再生成一把偏向锁,加锁后计算该对象的hashcode,观察前后对象布局。

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

HashCode: 28ba21f3
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 f3 21 ba (00000001 11110011 00100001 10111010) (-1172180223)
4 4 (object header) 28 00 00 00 (00101000 00000000 00000000 00000000) (40)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d8 f5 11 03 (11011000 11110101 00010001 00000011) (51508696)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 2e 03 (00000101 00111000 00101110 00000011) (53360645)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

HashCode: 694f9431
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ba 1d 08 2b (10111010 00011101 00001000 00101011) (721952186)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

根据上面结果可见:

  1. 偏向锁加锁前,计算hashcode,锁标志位不变,仍为 0 1,偏向锁位由 1变为 0,从偏向锁变为了普通对象,且mark word中确实存储了hashcode值。加锁后,锁标志位变为 0 0,即自旋锁。
  2. 偏向锁加锁后,计算harshcode,锁标志位变为 1 0,膨胀成重量级锁。

源码实现

java代码

1
2
3
4
5
6
7
8
9
10
11
12
public class T01_Synchronized {

static int i = 0;

public static void main(String[] args) {
Object lock = new Object();

synchronized (lock) {
i++;
}
}
}

字节码

通过jclasslib可以看到main函数的字节码如下(jclasslib的使用可移步《class文件格式-插件jclasslib》):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new #2 <java/lang/Object>
dup
invokespecial #1 <java/lang/Object.<init>>
astore_1
aload_1
dup
astore_2
monitorenter
getstatic #3 <S01_Synchronized/T01_Synchronized.i>
iconst_1
iadd
putstatic #3 <S01_Synchronized/T01_Synchronized.i>
aload_2
monitorexit
goto 30 (+8)
astore_3
aload_2
monitorexit
aload_3
athrow
return

可以看到 synchronized 锁住的同步代码段前后分别插入了一条monitorentermonitorexit指令。

可以注意到monitorexit指令有两条,这是因为如果在同步代码段发生了异常,会自动释放锁。

JVM实现

Lock Record数据结构

在hotspot源码(版本:hotspot-37240c1019fd)中,我们首先来看一下Lock Record的数据结构:

src\share\vm\runtime\basicLock.hpp中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock; // the lock, must be double word aligned
oop _obj; // object holds the lock;

public:
// Manipulation
oop obj() const { return _obj; }
void set_obj(oop obj) { _obj = obj; }
BasicLock* lock() { return &_lock; }

// Note: Use frame::interpreter_frame_monitor_size() for the size of BasicObjectLocks
// in interpreter activation frames since it includes machine-specific padding.
static int size() { return sizeof(BasicObjectLock)/wordSize; }

// GC support
void oops_do(OopClosure* f) { f->do_oop(&_obj); }

static int obj_offset_in_bytes() { return offset_of(BasicObjectLock, _obj); }
static int lock_offset_in_bytes() { return offset_of(BasicObjectLock, _lock); }
};

class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
volatile markOop _displaced_header;
public:
markOop displaced_header() const { return _displaced_header; }
void set_displaced_header(markOop header) { _displaced_header = header; }

void print_on(outputStream* st) const;

// move a basic lock (used during deoptimization
void move_to(oop obj, BasicLock* dest);

static int displaced_header_offset_in_bytes() { return offset_of(BasicLock, _displaced_header); }
};

可以看到 BasicObjectLock 中包含了 加锁对象_objLock Record _lock_lock中则包含了mark word的备份_displaced_head

再往下我们就能看到,BasicLock *就是自旋锁加锁时,实际存入mark word中的指针。

偏向锁

偏向锁的加锁过程就是MacroAssembler::biased_locking_enter函数,每个硬件平台有其各自的实现,如x86平台的实现在src\cpu\x86\vm\macroAssembler_x86.cpp文件中。

自旋锁

自旋锁的加锁过程为ObjectSynchronizer::slow_enter,在src\share\vm\runtime\synchronizer.cpp中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");

if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}

#if 0
// The following optimization isn't particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif

// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non-zero to avoid looking like a re-entrant lock,
// and must not look locked either.
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

可以看到,加锁流程为:

  1. 判断mark word,如果是普通对象,先将mark word存入lock record的displaced_header中,再通过cas操作将lock record的地址存入mark word中,如果成功,则加锁成功,如果失败,就膨胀为重量级锁。
  2. 如果判断mark word已经是自旋锁,那么通过is_lock_owned判断mark word中的lock record地址是否在本线程的栈中,如果在,则可重入,加锁成功;如果不在,膨胀为重量级锁。

从源码来看,似乎并未看到很多文章中描述的竞争加剧的条件,而是一旦自旋锁加锁失败,直接膨胀为重量级锁。