Fork me on GitHub

synchronized 实现原理

Synchronized的作用

  在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;
  synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
  Java中每一个非NULL的对象都可以作为锁,这是synchronized实现同步的基础:
  1.作用于普通方法时,锁是当前实例对象(this);
  2.作用于静态方法时,锁是当前类Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3.作用于同步代码块,锁是括号里面的对象

那么如何理解锁是“对象”?
  Java 编程语言中号称一切皆对象。当我们 new 一个对象的时候 JVM 会给 heap 中分配对象。HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
  对象头 这个头包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位、哈希码、锁状态等信息。第二部分存放指向方法区类静态数据的指针。”锁状态”就是用来同步操作的。因为锁信息是存储在对象上的,所以就不难理解 锁是对象 这句话了。
  JVM自动内存管理机制
那么 Java 为什么要将 锁 内置到对象中呢?
  这要从 monitor Object 设计模式说起.

monitor Object 设计模式

  问题描述:我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:
  1.在任一时间内,只有唯一的公共的成员方法,被唯一的线程所执行。
  2.对于对象的调用者来说,如果总是需要在调用方法之前进行拿锁,而在调用方法之后进行放锁,这将会使并发应用编程变得更加困难。
  3.如果一个对象的方法执行过程中,由于某些条件不能满足而阻塞,应该允许其它的客户端线程的方法调用可以访问该对象。
  如何解决:使用 Monitor Object 设计模式:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过 monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个 monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor 对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。
  monitor object 设计模式执行时序图中的红线部分 Monitor Object、Monitor Lock、Monitor Condition 三者就是 Java Object!! Java 将该模式内置到语言层面,对象加 Synchronized 关键字,就能确保任何对它的方法请求的同步被透明的进行,而不需要调用者的介入。
  这也就是为什么 Java 所有对象的基类 Object 中会有 wait()、notify()、notifyAll() 方法了。
  在使用synchronized、Object.wait()、Object.notify()/notifyAll()实现线程同步时,用到了两种机制:线程互斥锁mutex和条件变量condition

什么是Monitor?
  我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
  Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

synchronized反编译情况

1.同步代码块

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
public class SynchronizedTest {
public void test() {
synchronized (this) {
System.out.println("test");
}
}
}
// 反编译结果
changwendeMacBook-Pro:classes changwen$ javap -c SynchronizedTest.class
Compiled from "SynchronizedTest.java"
public class SynchronizedTest {
public SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String test
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
}

注意反编译的Code第3和第13指令
monitorenter :
  每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
  1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:
  objectref必须为reference类型数据。执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

2.同步方法

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
public class SynchronizedTest {
public synchronized void test1() {
}
public synchronized static void test2() {
}
}
// 反编译结果
changwendeMacBook-Pro:classes changwen$ javap -c SynchronizedTest.class
Compiled from "SynchronizedTest.java"
public class SynchronizedTest {
public SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public synchronized void test1();
Code:
0: return
public static synchronized void test2();
Code:
0: return
}

  从反编译的结果来看,对静态方法同步和方法块同步并没有 monitor 相关指令,方法级别的同步是隐式的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

指令补充:
  monitorenter:获得对象的锁, 用于同步方法或同步块
  monitorexit: 释放对象的锁, 用于同步方法或同步块
  invokevirtual:调用实例方法
  invokespecial:调用超类构建方法, 实例初始化方法, 私有方法
  invokestatic: 调用静态方法
  invokeinterface:调用接口方法
  invokedynamic: 调用动态方法

3、synchronized的实现原理

  当多线程访问时,就是通过对象头中的锁来同步的。访问过程如下图:
  
  它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
  1.Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  2.Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
  3.Wait Set:调用wait方法被阻塞的线程被放置在这里;
  4.OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
  5.Owner:当前已经获取到所资源的线程被称为Owner;
  6.!Owner:当前释放锁的线程。

  JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
  OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
  处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

  Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

  线程执行状态图
  
  一个常见的问题是 wait()、sleep()、yield() 方法的区别是什么?
  最大的不同在于 wait() 会释放对象锁,而 sleep()、yield() 不会,sleep() 是让当前线程休眠,而 yield() 是让出当前 CPU。


参考文章
  java并发:Synchronized 原理

-----------------本文结束,感谢您的阅读-----------------