线程安全之 synchronized 关键字

一般在遇到线程安全的问题时候,大家都经常会使用 synchronized 关键字,来达到线程互斥访问的目的,但是这也间接造成它被程序员滥用的局面。下面分析 synchronized 关键字的原理、用法以及注意事项,帮助大家更好地使用它。

synchronized 的用法

synchronized 关键字可以修饰代码块或者方法,是最基本的互斥同步手段,可以避免多个线程之间的竞争条件(race conditions)。

1
2
3
4
5
// 修饰代码块
synchronized (ReferenceType) {...}
// 修饰方法(包括静态方法)
synchronized MethodDeclaration

实现原理

synchronized 关键字可以使得同一时刻只有一个线程能访问一个代码块或者方法,但是其内部原理究竟是如何呢。要理解其原理,首先要知道一点:所有对象都自动含有单一的锁与其对应(每个已加载的类对象也对应一个锁),这个锁也叫作互斥锁。用 synchronized 修饰即代表着加了某一个对象的锁,修饰代码块时明确指明是什么对象的锁,而在修饰方法时,如果修饰成员方法,指被调用该方法对象的锁,如果修饰静态方法,指该 Class 类型的锁。

在线程执行到 synchronized 修饰的代码块或者方法时,会尝试获取一个对象的互斥锁,执行完后会释放这个锁。如果获取对象锁失败,当前线程就要阻塞等待,所以当一个线程持有一个对象的锁时,其他线程无法获得这个锁,从而达到互斥访问的目的。在执行 synchronized 方法或 synchronized 代码块时出现异常,JVM 会自动释放获取的锁。

从更底层分析的话,synchronized 关键字在经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。如果程序中的 synchronized 明确指明了对象参数,那就是这个对象的 reference ;如果没有明确指定,就根据 synchronized 修饰的是实例方法还是静态方法,去取对应的对象实例或 Class 对象来作为锁对象。

根据虚拟机规范,在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有该对象的锁,把锁的计数器加 1,相应的,在执行 monitorexit 指令时,会把锁的计数器减 1,当计数器为 0 时,锁就被释放。

根据上面原理分析,需要注意下面两点:一是,同一线程在已经获取一个对象锁时,可以多次获取该对象时,实际内部只是把锁的计数器加 1,并不会因此造成死锁 ;二是,当一个线程获取一个对象锁时,其他线程仍然可以访问该对象的公有成员变量或调用该对象的 un-synchronized 方法,因为这些操作不需要获取该对象锁。

使用注意事项

在使用 synchronized 的过程中,想要达到线程互斥访问的目的,最关键的是清楚所申请的是不是同一个对象的锁,只有申请的是同一个对象的锁时才能同一个时刻只有一个线程访问。下面的例子分别描述各种写法所申请的对象锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Foo {
public void method1() {
synchronized (this) { // 调用 foo.method1() 方法时,锁的是 foo 对象
...
}
}
public void method2() {
synchronized (Foo.class) { // 调用 foo.method2() 方法时,锁的是 Foo.class 对象
...
}
}
public synchronized void method3() {..} // 调用 foo.method3() 方法时,锁的是 foo 对象
public static synchronized void method4() {...} // 锁的是 Foo.class 对象
}

class 与 getClass

网上有个流行甚广的说法:“记得在《Effective Java》一书中看过 Foo.class 和 foo1.getClass() 用于作同步锁还不相同,不能用 foo1.getClass() 来达到锁这个 Class 的目的”

经过测试 JDK 1.6 之后,两者锁的是同一个 Class 对象,官方关于 getClass 的定义如下:

Returns the runtime class of this Object. The returned Class object is the object that is locked by static synchronized methods of the represented class.

所以这两种写法与静态同步方法锁的都是同一个 Class 对象,和synchronized (Foo.class)的意义一样。

synchronized 的缺陷

synchronized 关键字不是万能的,也有一定的缺陷,在适合的场景可以用更轻量级的 volatile 替代。

性能问题

Java 的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态装换到核心态,状态转换会耗费很多的处理器时间,所以 synchronized 是 Java 语言中一个重量级的操作。

死锁问题

Thread1 先获取锁 lockA,然后在同步块中嵌套竞争 lockB,而 Thread2 先获取 lockB,然后在同步块中嵌套竞争 lockA。此时 Thread1 等待被 Thread2 拥有的 lockB,Thread2 又在等待被 Thread1 拥有的 lockA,这样就造成死锁了。在实际编程中需要注意这种情况,避免出现死锁。

参考文章