前言
Java 自首个版本便提供了多线程的支持,并为开发者提供了synchronized、volatile
关键字用于解决并发下线程数据同步的问题。在Java 5 以前开发者也只能使用这两个关键词解决同步问题,比较“简单粗暴”缺乏灵活性。在Java 5 java.util.concurrent
包诞生后才有了更多的选择,在后续文章会介绍。本文是作者自己对synchronized、volatile
关键字的理解与总结,不对之处,望指出,共勉。
synchronized
Java 在 Java 5 以前通过synchronized
关键词用来实现锁,由于其由JVM指令隐式实现也被称为隐式锁。不过由于其实现过于底层,所以对性能的影响较大,但随着Java 6 对其进行了一些优化后,有了一定改善。synchronized
关键字是解决本系列第一篇文章说到的互斥性、原子性、可见性、有序性问题的最直接、简单的方式。
使用
synchronized
关键字可以用在代码块和方法上,表示在执行整个代码块或方法之前线程必须取得相应对象的锁(monitor)。被synchronized
修饰的代码块或方法,每次只允许一个线程进入执行,如果其他线程试图进入,JVM会将它们挂起(放入到等锁池中)。这种结构在并发理论中称为临界区(critical section)。
- 修饰成员方法,线程要取得类的当前实例对象的锁方可执行
public class SynchronizedMethodTest {
public /*synchronized*/ void method1() {
try {
//模拟方法需要执行100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method1() execute!");
}
public /*synchronized*/ void method2() {
System.out.println("method2() execute!");
}
public static void main(String[] args) {
final SynchronizedMethodTest test = new SynchronizedMethodTest();
// 使用Java 8 lambda 简化代码
new Thread(() -> test.method1()).start();
new Thread(() -> test.method2()).start();
/**
输出:
method2() execute!
method1() execute!
使用synchronized修饰方法后:
method1() execute!
method2() execute!
**/
}
}
- 修饰静态方法,线程要取得类的Class对象的锁方可执行
public class SynchronizedStaticMethodTest {
public /*synchronized*/ static void method1() {
try {
//模拟方法需要执行100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method1() execute!");
}
public /*synchronized*/ static void method2() {
System.out.println("method2() execute!");
}
public static void main(String[] args) {
final SynchronizedStaticMethodTest test1 = new SynchronizedStaticMethodTest();
final SynchronizedStaticMethodTest test2 = new SynchronizedStaticMethodTest();
// 使用Java 8 lambda 简化代码
new Thread(() -> test1.method1()).start();
new Thread(() -> test2.method2()).start();
new Thread(() -> SynchronizedStaticMethodTest.method1()).start();
new Thread(() -> SynchronizedStaticMethodTest.method2()).start();
/**
输出:
method2() execute!
method2() execute!
method1() execute!
method1() execute!
使用synchronized修饰方法后:
method1() execute!
method2() execute!
method1() execute!
method2() execute!
**/
}
}
- 修饰代码块,程序员可以指定要取得的是哪个对象(包括Class对象)的锁,线程需获得该锁方可执行
public class SynchronizedCodeBlockTest {
private final Object lock = new Object();
public void method1() {
//需获得Class对象的锁方可执行
//synchronized (this.getClass())
//需获得lock对象的锁方可执行
//synchronized (lock)
//需获得当前对象的锁方可执行
synchronized (this) {
try {
//模拟方法需要执行100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method1() execute!");
}
}
public void method2() {
//需获得Class对象的锁方可执行
//synchronized (this.getClass())
//需获得lock对象的锁方可执行
//synchronized (lock)
//需获得当前对象的锁方可执行
synchronized (this) {
System.out.println("method2() execute!");
}
}
public static void main(String[] args) {
final SynchronizedCodeBlockTest test = new SynchronizedCodeBlockTest();
// 使用Java 8 lambda 简化代码
new Thread(() -> test.method1()).start();
new Thread(() -> test.method2()).start();
/**
输出:
method1() execute!
method2() execute!
*/
}
}
实现原理
Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).
For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10).
上面这段话来自The Java® Virtual Machine Specification 3.14. Synchronization,简单来说就是JVM使用monitor(监视器锁)来实现同步(synchronized
关键字),其中同步代码块采用monitorenter
、monitorexit
指令显式的实现,而同步方法则使用ACC_SYNCHRONIZED
标记符隐式的实现。下面通过javap
反汇编指令查看一段简单的代码,看看是否如此。
public class SynchronizedTest {
public synchronized void method1(){
System.out.println("Hello World!");
}
public void method2(){
synchronized (this){
System.out.println("Hello World!");
}
}
}
$ javap -v concurrent/target/classes/sync/SynchronizedTest.class
Classfile /E:/IdeaWorkSpace/java-codes/concurrent/target/classes/sync/SynchronizedTest.class
Last modified 2017-6-14; size 702 bytes
MD5 checksum 48cb43f462459cc1eed4ba2e7c1d204a
Compiled from "SynchronizedTest.java"
public class sync.SynchronizedTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
...略
{
...略
public synchronized void method1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED//同步方法的实现
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lsync/SynchronizedTest;
public void method2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
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 Hello World!
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:
...
LineNumberTable:
...略
LocalVariableTable:
...略
StackMapTable: number_of_entries = 2
...略
}
SourceFile: "SynchronizedTest.java"
通过查看字节码的反汇编结果,果然如此,下面是JVM规范对monitorenter
、monitorexit
的描述,我做了一个简单的翻译,你可以点击标题查看原文。
每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter
指令时会尝试获取相应对象的monitor,获取规则如下:
- 如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
- 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以
synchronized
关键字实现的锁是可重入的锁。 - 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。
只有拥有相应对象的monitor的线程才能执行monitorexit
指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
总结
- 只能锁定对象,不能锁定基本数据类型
- 被锁定的对象数组中的单个对象不会被锁定
- 同步方法可以视为包含整个方法的
synchronized(this) { … }
代码块 - 静态同步方法会锁定它的Class对象
- 内部类的同步是独立于外部类的
-
synchronized
修饰符并不是方法签名的组成部分,所以不能出现在接口的方法声明中 - 非同步的方法不关心锁的状态,它们在同步方法运行时仍然可以得以运行
- 代码块同步使用
monitorenter
和monitorexit
指令实现 - 方法同步使用
ACC_SYNCHRONIZED
标记符实现 -
synchronized
实现的锁是可重入锁、悲观锁、独占锁、互斥锁
volatile
volatile
关键字相对于synchronized
是一种简单的同步机制,常用于解决可见性问题(可见性指的是当一个线程对共享变量进行更改后,其他线程对更改后的值是可见的),因为被volatile
修饰的变量遵循以下规则:
- 变量的值在使用之前总会从主内存中再读取出来。
- 对变量值的修改总会在完成之后写回到主内存中。
使用volatile
关键字可以在多线程环境下预防编译器不正确的优化假设(编译器可能会将在一个线程中值不会发生改变的变量优化成常量),但只有修改时不依赖当前状态(读取时的值)的变量才应该声明为volatile
变量。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
//上面伪代码中 while(!stop) 有可能会被编译器优化为 while(true),进而不能被其他线程中断,导致死循环。
使用
解决可见性问题
public class VolatileTest {
private /*volatile*/ int sharedValue = 0;
public static void main(String[] args) throws InterruptedException {
VolatileTest test = new VolatileTest();
new Thread(() -> test.listener()).start();
new Thread(() -> test.increment()).start();
/**
输出:
Value Incrementing:1
Value Incrementing:2
Value Incrementing:3
Value Incrementing:4
Value Incrementing:5
使用 volatile 修饰 sharedValue后:
Value Incrementing:1
Value Changed:1
Value Incrementing:2
Value Changed:2
Value Incrementing:3
Value Changed:3
Value Incrementing:4
Value Changed:4
Value Incrementing:5
Value Changed:5
*/
}
public void listener() {
int localValue = sharedValue;
while (sharedValue < 5) {
if (localValue != sharedValue) {
System.out.println("Value Changed:" + sharedValue);
localValue = sharedValue;
}
}
}
public void increment() {
while (sharedValue < 5) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
++sharedValue;
System.out.println("Value Incrementing:" + sharedValue);
}
}
}
实现双重检查(Double-Checked)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
new AssertionError("don't support reflect.");
}
public static Singleton getInstance() {
if (instance == null) { // Single Checked
synchronized (Singleton.class) {
if (instance == null) { // Double checked
instance = new Singleton();
}
}
}
return instance;
}
}
实现原理
“ 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令 ” -- 《深入理解Java虚拟机》
lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
推荐阅读:深入分析Volatile的实现原理
总结
- 可用于解决可见性问题
- 可禁止编译器和处理器对指令进行重排序,能在一定程度上解决有序性问题
- 不能解决原子性问题