Preface {#preface}
本文中使用的 JDK 版本是 JDK 11.0.2
。
synchronized {#synchronized}
在 Java 多线程编程中需要关注的一个重中之重的问题便是线程安全问题。
线程安全问题的主要诱因有如下两点:
- 存在共享数据(也称临界资源);
- 存在多条线程共同操作这些共享数据。
解决此问题的根本方法:同一时刻有且只有一个线程在操作共享数据,其它线程必须等到该线程处理完数据后再对共享数据进行操作。
由此发明了互斥锁的概念。
互斥锁的特性:
- 互斥性:即在同一时间只允许一个线程持有某一个对象锁,通过这种特性来实现多线程的协调机制,确保在同一时间内只有一个线程对同步代码块(复合操作)进行访问。互斥性也称为操作的原子性;
- 可见性:必须确保锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得共享变量最新的值),否则另一个线程可能是在某个本地缓存的副本上继续操作,从而引起数据的不一致的问题。
Java 中的关键字 synchronized
满足了上述要求,它可以保证在某一个时刻只有一个线程能够执行某个方法或代码块,也可以保证共享数据的变化能够被其它线程所看到。
注意: synchronized
锁的不是代码,锁住的都是对象。
堆中的数据是所有线程共享的,也是和我们打交道最多的区域,因此恰当合理的给对象上锁是解决问题的关键。
synchronized
根据获取的锁分类可分为:获取对象锁和获取类锁。
获取对象锁的两种用法:
- 同步代码块(
synchronized(this)
,synchronized(类实例对象)
),锁是小括号()
中的实例对象; - 同步非静态方法(
synchronized method
),锁是当前对象的实例对象。
以下例子展示了同步代码块与同步非静态方法之间的区别与联系:
首先编写 SyncThread
实现 Runnable
接口,用来执行异步方法、同步代码块方法、同步方法。
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {
// 执行异步方法
async();
} else if (threadName.startsWith("B")) {
// 执行有 synchronized(this|object) {} 同步代码块方法
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
// 执行synchronized 修饰非静态方法
syncObjectMethod1();
}
}
/**
* 异步方法
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态方法
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
编写测试类,多线程执行 SyncThread
类,传入同一把对象锁 syncThread
对象。
public class SyncDemo {
public static void main(String[] args) {
SyncThread syncThread = new SyncThread();
// 异步方法、同步块方法、同步方法各创建两个线程,传入相同的对象锁 syncThread,它们共享同一把锁
// 异步方法
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
// 同步代码块方法
Thread B_thread1 = new Thread(syncThread, "B_thread1");
Thread B_thread2 = new Thread(syncThread, "B_thread2");
// 同步方法
Thread C_thread1 = new Thread(syncThread, "C_thread1");
Thread C_thread2 = new Thread(syncThread, "C_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
}
/* 输出:
A_thread2_Async_Start: 14:31:28
C_thread1_SyncObjectMethod1: 14:31:28
A_thread1_Async_Start: 14:31:28
B_thread1_SyncObjectBlock1: 14:31:28
C_thread1_SyncObjectMethod1_Start: 14:31:28
B_thread2_SyncObjectBlock1: 14:31:28
C_thread1_SyncObjectMethod1_End: 14:31:29
A_thread2_Async_End: 14:31:29
A_thread1_Async_End: 14:31:29
B_thread2_SyncObjectBlock1_Start: 14:31:29
B_thread2_SyncObjectBlock1_End: 14:31:30
B_thread1_SyncObjectBlock1_Start: 14:31:30
B_thread1_SyncObjectBlock1_End: 14:31:31
C_thread2_SyncObjectMethod1: 14:31:31
C_thread2_SyncObjectMethod1_Start: 14:31:31
C_thread2_SyncObjectMethod1_End: 14:31:32
*/
}
分析输出结果:
对于 A 类线程来说当 A1 线程启动之后输出 A_thread1_Async_Start: 14:31:28
,A2 线程并未等待 A1 线程 End
就 Start
了,说明 A 类线程是异步执行的,且由于没有获取同步锁的需求,它也不受 B 类,C 类线程的影响。
对于 B 类线程和 C 类线程来说,由于需要获取同步锁的缘故,总是要等到一个线程执行完毕输出 End
释放同步锁之后,另外的线程才能获取到同步锁执行同步代码输出 Start
。在输出结果中过滤掉 A 类线程的输出干扰后就一目了然了:
C_thread1_SyncObjectMethod1: 14:31:28
B_thread1_SyncObjectBlock1: 14:31:28
C_thread1_SyncObjectMethod1_Start: 14:31:28
B_thread2_SyncObjectBlock1: 14:31:28
C_thread1_SyncObjectMethod1_End: 14:31:29
B_thread2_SyncObjectBlock1_Start: 14:31:29
B_thread2_SyncObjectBlock1_End: 14:31:30
B_thread1_SyncObjectBlock1_Start: 14:31:30
B_thread1_SyncObjectBlock1_End: 14:31:31
C_thread2_SyncObjectMethod1: 14:31:31
C_thread2_SyncObjectMethod1_Start: 14:31:31
C_thread2_SyncObjectMethod1_End: 14:31:32
说明 synchronized
修饰的非静态方法与同步代码块方法中的 synchronized(this)
锁的是同一个对象,即 this
对象。
另外 B 类对象同步代码块之外的输出代码:B_thread1_SyncObjectBlock1: 14:31:28
和 B_thread2_SyncObjectBlock1: 14:31:28
并不受同步锁的影响,因为在它们之前有 C_thread1_SyncObjectMethod1: 14:31:28
输出,说明 C1 线程已经获取到了同步锁,而 B1 和 B2 线程照样输出。
C 类线程的 C_thread1_SyncObjectMethod1: 14:31:28
和 C_thread2_SyncObjectMethod1: 14:31:31
也是受同步锁影响的,因为它们总是按照顺序输出,即获取到锁之后才能从方法中的第一行代码开始执行。
C_thread1_SyncObjectMethod1: 14:31:28
C_thread1_SyncObjectMethod1_Start: 14:31:28
C_thread1_SyncObjectMethod1_End: 14:31:29
C_thread2_SyncObjectMethod1: 14:31:31
C_thread2_SyncObjectMethod1_Start: 14:31:31
C_thread2_SyncObjectMethod1_End: 14:31:32
如果每个线程传递的对象锁都不一样,再看看看又会有怎样的输出:
public class SyncDemo {
public static void main(String[] args) {
// 异步方法、同步块方法、同步方法各创建两个线程,每个线程传入不同的对象锁
// 异步方法
Thread A_thread1 = new Thread(new SyncThread(), "A_thread1");
Thread A_thread2 = new Thread(new SyncThread(), "A_thread2");
// 同步代码块方法
Thread B_thread1 = new Thread(new SyncThread(), "B_thread1");
Thread B_thread2 = new Thread(new SyncThread(), "B_thread2");
// 同步方法
Thread C_thread1 = new Thread(new SyncThread(), "C_thread1");
Thread C_thread2 = new Thread(new SyncThread(), "C_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
}
/* 输出:
A_thread1_Async_Start: 14:58:37
B_thread2_SyncObjectBlock1: 14:58:37
A_thread2_Async_Start: 14:58:37
C_thread2_SyncObjectMethod1: 14:58:37
C_thread1_SyncObjectMethod1: 14:58:37
B_thread1_SyncObjectBlock1: 14:58:37
B_thread2_SyncObjectBlock1_Start: 14:58:37
C_thread2_SyncObjectMethod1_Start: 14:58:37
B_thread1_SyncObjectBlock1_Start: 14:58:37
C_thread1_SyncObjectMethod1_Start: 14:58:37
A_thread1_Async_End: 14:58:38
B_thread1_SyncObjectBlock1_End: 14:58:38
C_thread2_SyncObjectMethod1_End: 14:58:38
A_thread2_Async_End: 14:58:38
C_thread1_SyncObjectMethod1_End: 14:58:38
B_thread2_SyncObjectBlock1_End: 14:58:38
*/
}
运行之后发现所有线程都变成了异步执行,并未等线程输出 End
释放掉同步锁之后其它线程获取到同步锁后再输出 Start
。
因为它们所持有的同步锁对象各不相同,互相锁不住。
B_thread2_SyncObjectBlock1_Start: 14:58:37
C_thread2_SyncObjectMethod1_Start: 14:58:37
B_thread1_SyncObjectBlock1_Start: 14:58:37
C_thread1_SyncObjectMethod1_Start: 14:58:37
获取类锁的两种方法:
- 同步代码块(
synchronized(类.class)
),锁是小括号()
中的类对象(Class
对象); - 同步静态方法(
synchronized static method
),锁是当前对象的类对象(Class对象)。
其实类锁也是一种对象锁,只是比较特殊所有的实例共享同一个 Class
对象作为锁。
修改上面对象锁的例子,改为类锁,添加 syncClassBlock1
和 syncClassMethod1
这两个方法。
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {
// 执行异步方法
async();
} else if (threadName.startsWith("B")) {
// 执行有 synchronized(this|object) {} 同步代码块方法
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
// 执行 synchronized 修饰非静态方法
syncObjectMethod1();
} else if (threadName.startsWith("D")) {
// 执行有 synchronized (SyncThread.class) {} 同步代码块方法
syncClassBlock1();
} else if (threadName.startsWith("E")) {
// 执行 synchronized 修饰静态方法
syncClassMethod1();
}
}
/**
* 异步方法
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 方法中有 synchronized (SyncThread.class) {} 同步代码块
*/
private void syncClassBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态方法
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* synchronized 修饰静态方法
*/
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类,传入同一个对象,测试类锁的行为。
ublic class SyncDemo {
public static void main(String[] args) {
// 异步方法、同步块方法、同步方法各创建两个线程,每个线程传入同一个对象
SyncThread syncThread = new SyncThread();
// 异步方法
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
// 同步代码块方法
// Thread B_thread1 = new Thread(new SyncThread(), "B_thread1");
// Thread B_thread2 = new Thread(new SyncThread(), "B_thread2");
// 同步方法
// Thread C_thread1 = new Thread(new SyncThread(), "C_thread1");
// Thread C_thread2 = new Thread(new SyncThread(), "C_thread2");
// 类锁同步代码块方法
Thread D_thread1 = new Thread(syncThread, "D_thread1");
Thread D_thread2 = new Thread(syncThread, "D_thread2");
// 静态同步方法
Thread E_thread1 = new Thread(syncThread, "E_thread1");
Thread E_thread2 = new Thread(syncThread, "E_thread2");
A_thread1.start();
A_thread2.start();
// B_thread1.start();
// B_thread2.start();
// C_thread1.start();
// C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
}
/* 输出:
A_thread1_Async_Start: 15:14:59
A_thread2_Async_Start: 15:14:59
E_thread1_SyncClassMethod1: 15:14:59
D_thread1_SyncClassBlock1: 15:14:59
D_thread2_SyncClassBlock1: 15:14:59
E_thread1_SyncClassMethod1_Start: 15:14:59
A_thread2_Async_End: 15:15:00
A_thread1_Async_End: 15:15:00
E_thread1_SyncClassMethod1_End: 15:15:00
D_thread2_SyncClassBlock1_Start: 15:15:00
D_thread2_SyncClassBlock1_End: 15:15:01
D_thread1_SyncClassBlock1_Start: 15:15:01
D_thread1_SyncClassBlock1_End: 15:15:02
E_thread2_SyncClassMethod1: 15:15:02
E_thread2_SyncClassMethod1_Start: 15:15:02
E_thread2_SyncClassMethod1_End: 15:15:03
*/
}
从测试结果看,类锁表现的行为和传入同一对象,对象锁的行为一致。
如果传入不同的 SyncThread
对象(它们的类是一样的)又会如何?
public class SyncDemo {
public static void main(String[] args) {
// 异步方法、同步块方法、同步方法各创建两个线程,每个线程传入不同的对象
// 异步方法
Thread A_thread1 = new Thread(new SyncThread(), "A_thread1");
Thread A_thread2 = new Thread(new SyncThread(), "A_thread2");
// 同步代码块方法
// Thread B_thread1 = new Thread(new SyncThread(), "B_thread1");
// Thread B_thread2 = new Thread(new SyncThread(), "B_thread2");
// 同步方法
// Thread C_thread1 = new Thread(new SyncThread(), "C_thread1");
// Thread C_thread2 = new Thread(new SyncThread(), "C_thread2");
// 类锁同步代码块方法
Thread D_thread1 = new Thread(new SyncThread(), "D_thread1");
Thread D_thread2 = new Thread(new SyncThread(), "D_thread2");
// 静态同步方法
Thread E_thread1 = new Thread(new SyncThread(), "E_thread1");
Thread E_thread2 = new Thread(new SyncThread(), "E_thread2");
A_thread1.start();
A_thread2.start();
// B_thread1.start();
// B_thread2.start();
// C_thread1.start();
// C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
}
/* 输出:
D_thread2_SyncClassBlock1: 15:20:25
A_thread2_Async_Start: 15:20:25
E_thread1_SyncClassMethod1: 15:20:25
D_thread1_SyncClassBlock1: 15:20:25
A_thread1_Async_Start: 15:20:25
E_thread1_SyncClassMethod1_Start: 15:20:25
A_thread2_Async_End: 15:20:26
E_thread1_SyncClassMethod1_End: 15:20:26
A_thread1_Async_End: 15:20:26
D_thread1_SyncClassBlock1_Start: 15:20:26
D_thread1_SyncClassBlock1_End: 15:20:27
D_thread2_SyncClassBlock1_Start: 15:20:27
D_thread2_SyncClassBlock1_End: 15:20:28
E_thread2_SyncClassMethod1: 15:20:28
E_thread2_SyncClassMethod1_Start: 15:20:28
E_thread2_SyncClassMethod1_End: 15:20:29
*/
}
从测试结果看,即便传入的对象各不相同(但类相同),所表现出来的行为也和上面一致。即 A 类线程依旧我行我素不受其它方法的影响,D 类和 E 类线程的类锁同步代码块和静态同步方法都是顺序执行的。
由于类锁是一个特殊的对象锁,当类锁和对象锁都存在时它们是互不干扰的,因为它们锁的不是同一个东西。
public class SyncDemo {
public static void main(String[] args) {
// 异步方法、同步块方法、同步方法各创建两个线程,每个线程传入同一对象
SyncThread syncThread = new SyncThread();
// 异步方法
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
// 同步代码块方法
Thread B_thread1 = new Thread(syncThread, "B_thread1");
Thread B_thread2 = new Thread(syncThread, "B_thread2");
// 同步方法
Thread C_thread1 = new Thread(syncThread, "C_thread1");
Thread C_thread2 = new Thread(syncThread, "C_thread2");
// 类锁同步代码块方法
Thread D_thread1 = new Thread(syncThread, "D_thread1");
Thread D_thread2 = new Thread(syncThread, "D_thread2");
// 静态同步方法
Thread E_thread1 = new Thread(syncThread, "E_thread1");
Thread E_thread2 = new Thread(syncThread, "E_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
}
/* 输出:
A_thread2_Async_Start: 15:27:19
C_thread1_SyncObjectMethod1: 15:27:19
A_thread1_Async_Start: 15:27:19
D_thread1_SyncClassBlock1: 15:27:19
E_thread1_SyncClassMethod1: 15:27:19
D_thread2_SyncClassBlock1: 15:27:19
B_thread2_SyncObjectBlock1: 15:27:19
B_thread1_SyncObjectBlock1: 15:27:19
E_thread1_SyncClassMethod1_Start: 15:27:19
C_thread1_SyncObjectMethod1_Start: 15:27:19
C_thread1_SyncObjectMethod1_End: 15:27:20
A_thread2_Async_End: 15:27:20
E_thread1_SyncClassMethod1_End: 15:27:20
A_thread1_Async_End: 15:27:20
B_thread1_SyncObjectBlock1_Start: 15:27:20
D_thread2_SyncClassBlock1_Start: 15:27:20
B_thread1_SyncObjectBlock1_End: 15:27:21
D_thread2_SyncClassBlock1_End: 15:27:21
B_thread2_SyncObjectBlock1_Start: 15:27:21
D_thread1_SyncClassBlock1_Start: 15:27:21
D_thread1_SyncClassBlock1_End: 15:27:22
B_thread2_SyncObjectBlock1_End: 15:27:22
E_thread2_SyncClassMethod1: 15:27:22
C_thread2_SyncObjectMethod1: 15:27:22
E_thread2_SyncClassMethod1_Start: 15:27:22
C_thread2_SyncObjectMethod1_Start: 15:27:22
C_thread2_SyncObjectMethod1_End: 15:27:23
E_thread2_SyncClassMethod1_End: 15:27:23
*/
}
当 C1 线程获取对象锁之后 C_thread1_SyncObjectMethod1: 15:27:19
,E1 线程并没有等待其执行完毕就开始执行了 E_thread1_SyncClassMethod1: 15:27:19
。
传入不同的 SyncThread
对象也是一样,不再赘述。
对象锁和类锁的总结:
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此表现和上述 1,2,3,4 一致,并且由于一个类只有一把对象锁,所以同一类的不同对象使用类锁将会是同步的;
- 类锁和对象锁互不干扰。
synchronized 底层实现原理 {#synchronized-底层实现原理}
实现 synchronized
的基础:
- Java 对象头
- Monitor
对象在内存中的布局:
- 对象头
- 实例数据
- 对齐填充
这里重点说下对象头。
一般来说 synchronized
使用的锁对象是存储在 Java 对象头中,主要结构由 Mark Word
和 Class Metadata Address
组成。其中 Class Metadata Address
是对象指向它类 元数据的指针,虚拟机通过这个指针确定对象属于哪个类的实例;而 Mark Word
用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
对象头的结构为:
由于对象头信息的存储与对象自身定义数据的存储没有关联,所以它是额外的存储成本,考虑到 JVM 空间效率, Mark Word
被设计成非固定的数据结构以便存储更多有效数据,它会根据对象本身的状态复用自己的存储空间。如 32 位 JVM 下的 Mark Word
除了有图中的蓝色部分的默认存储结构外,还有 JDK 6 添加的轻量级锁和偏向锁等。
Monitor:每个 Java 对象天生自带了一把看不见的锁,叫做内部锁或者 Monitor
。
Monitor
也被称之为管程或监视器锁。可以将其理解为一个同步工具或一种同步机制,通常被描述为一个对象。
如上图中的重量级锁中的指针指向就是 Monitor
对象的起始地址,每个对象都存在一个 Monitor
与之关联,对象与其 Monitor
之间有多种实现方式,如:Monitor
与对象一起创建销毁,线程试图获取对象锁时自动生成等。
当一个 Monitor
被某个线程持有后,它便会处于锁定状态。
在 HotSpot
虚拟机中 Monitor
是由 ObjectMonitor
实现的,由 C++ 语言编写。
在 ObjectMonitor
中有两个集合 _WaitSet
和 _EntryList
(即等待池和锁池),它们用来保存 ObjectMonitor
的对象列表,每个对象锁的线程都会被封装成 ObjectMonitor
保存到里面。
_owner
字段指向持有 ObjectMonitor
对象的线程,当多个线程同时访问同一个同步代码时,首先会进入到 _EntryList
集合中,当线程获取到对象的 Monitor
后就进入到 ObjectMonitor
区域并将其 _owner
变量的值设置为当前线程,同时计数器 _count
+1;若线程调用 wait()
方法将会释放当前持有的 Monitor
,_owner
变量会重新设为 null
,_count
也会-1,该线程实例会被放入 _WaitSet
集合中等待被唤醒;若当前线程执行完毕也会释放 Monitor
锁并复位对应变量的值,以便其它线程获取 Monitor
锁。
Monitor 锁的竞争、获取与释放:
Monitor
对象存在每个 Java 对象的对象头中,synchronized
便是通过这种方式来获取锁的,也是 Java 中的任意对象都能作为锁的原因。
下面这个例子演示了 synchronized
在字节码层面的具体语义实现:
public class SyncBlockAndMethod {
public void syncsTask() {
synchronized (this) {
System.out.println("Hello");
}
}
public synchronized void syncTask() {
System.out.println("Hello Again");
}
}
使用 javac
命令编译此源码文件:
javac cool\ldw\javabasic\thread\SyncBlockAndMethod.java
使用 javap
命令查看生成的 .class
字节码文件:
javap -verbose cool\ldw\javabasic\thread\SyncBlockAndMethod.class
输出内容如下:
Classfile /D:/javabasic/src/cool/ldw/javabasic/thread/SyncBlockAndMethod.class
Last modified 2024年3月5日; size 626 bytes
SHA-256 checksum 3639b3877f2a1b3d60887ca9f52d204bd8fa30bc259f62c3997be41ce80f6d8f
Compiled from "SyncBlockAndMethod.java"
public class cool.ldw.javabasic.thread.SyncBlockAndMethod
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #23 // cool/ldw/javabasic/thread/SyncBlockAndMethod
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."`<init>`":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "`<init>`":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 `<init>`
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello
#14 = Utf8 Hello
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = String #22 // Hello Again
#22 = Utf8 Hello Again
#23 = Class #24 // cool/ldw/javabasic/thread/SyncBlockAndMethod
#24 = Utf8 cool/ldw/javabasic/thread/SyncBlockAndMethod
#25 = Utf8 Code
#26 = Utf8 LineNumberTable
#27 = Utf8 syncsTask
#28 = Utf8 StackMapTable
#29 = Class #30 // java/lang/Throwable
#30 = Utf8 java/lang/Throwable
#31 = Utf8 syncTask
#32 = Utf8 SourceFile
#33 = Utf8 SyncBlockAndMethod.java
{
public cool.ldw.javabasic.thread.SyncBlockAndMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."`<init>`":()V
4: return
LineNumberTable:
line 3: 0
public void syncsTask();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
// monitorenter 指令指向同步代码块的开始位置
// 当执行 monitorenter 指令时,当前线程将试图获取对象锁,即对象头中 Monitor 的持有权
// 当 Monitor 的进入计数器(_count 变量)为 0 时线程就可以成功获取到 Monitor 的持有权,并将进入计数器设置为 1,表示取锁成功
// 如果当前线程在之前已经拥有了对象锁中 Monitor 的持有权,此时它可以重入 Monitor
// 如果其它线程先于当前线程拥有了 Monitor 的持有权,那么当前线程只能被阻塞在此,直到其它线程释放掉 Monitor 的持有权后才有机会参与竞争持有 Monitor
3: monitorenter
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #13 // String Hello
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
// monitorexit 指令指向同步代码块的结束位置
// 当执行 monitorexit 指令时将释放 Monitor 锁,并设置计数器(_count 变量)为 0
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
// 为了保证执行过程中发生异常时也能够正确执行 monitorexit 指令,编译器会自动生成一个可处理所有异常的异常处理器
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 12
line 8: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class cool/ldw/javabasic/thread/SyncBlockAndMethod, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void syncTask();
descriptor: ()V
// 方法级的同步是隐式的,无需通过字节码指令来控制,这里设置了 ACC_SYNCHRONIZED 访问标志用来区分该方法是否为同步方法
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String Hello Again
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
}
SourceFile: "SyncBlockAndMethod.java"
什么是重入?
从互斥锁的设计上来说,当一个线程试图操作一个由其它线程持有的对象锁的林间资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况输入重入,请求将会成功。
在 Java 中
synchronized
是基于原子性的内部锁机制,是可重入的,在一个线程调用synchronized
方法(或同步代码块)时在其方法体(或同步代码块)内部调用另一个synchronized
方法(或同步代码)是允许的。举个例子:
public void syncsTask() { synchronized (this) { System.out.println("Hello"); synchronized (this) { System.out.println("World!"); } } }
第二个
synchronized (this)
不会被锁住。
为什么会对 synchronized
嗤之以鼻?
- 早期版本中,
synchronized
属于重量级锁,依赖Mutex Lock
实现; - 线程之间的切换需要从用户态转换到核心态,开销较大。
Java 6 以后,synchronized
性能得到了很大的提升, HotSpot
虚拟机花费大量精力实现各种锁优化技术,如:
- Adaptive Spinning:自适应自旋;
- Lock Eliminate:锁消除;
- Lock Coarsening:锁粗化;
- Lightweight Locking:轻量级锁;
- Biased Locking:偏向锁。
这些技术都是为了在线程间更高效的共享数据,解决竞争问题,从而提高程序执行效率。
自旋锁 {#自旋锁}
- 在许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得;
- 通过让线程执行忙循环(类似
while(true)
)等待锁的释放,不让出 CPU 执行时间; - 缺点:若锁被其它线程长时间占用,会带来许多性能上的开销,因为未获取到锁的线程一直在自旋;
- 可以使用
PreBlockSpin
参数更改等待的自旋次数,如果超过了此次数仍然未获取到锁,则切换为传统方式挂起线程。
自适应自旋锁 {#自适应自旋锁}
在自旋锁中会有这样一个问题,线程每次等待的时间都不固定导致设置合理的 PreBlockSpin
参数值比较困难。
因此需要更聪明的锁实现更加灵活的自旋,提高并发性能,这便是自适应自旋锁,它在 Java 6 中被引入。
-
自旋的次数不再固定;
-
由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定;
如果在同一个锁对象上自旋等待时,刚刚成功获取到锁,并且持有锁的线程正在运行中,那么 JVM 会认为该锁自旋获取到的可能性很大,会自动增加自旋等待次数,比如增加到 50 次循环。
相反,如果对于某个锁自旋很少成功获取到锁,那么在之后获取锁时可能会省略掉自旋过程避免浪费处理器资源。
锁消除 {#锁消除}
- JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
举个例子:
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
// StringBuffer 线程安全,由于 sb 没有 return 出去给其它方法使用,只会在 append 方法中使用,不可能被其它线程引用
// 因此 sb 对象属于不可能共享的资源,JVM 会自动消除 append() 方法内部的锁。
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
锁粗化 {#锁粗化}
如果一连串操作都对同一对象反复加锁和解锁,甚至加锁操作出现在循环体中,那么即便没有线程竞争,频繁进行互斥同步锁操作也会导致不必要的性能浪费,为了避免此现象的方法就有了锁粗化。
- 通过扩大加锁的范围,避免频繁的加锁和解锁。
举个例子:
public class CoarseSync {
public static String copyString100Times(String target) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
// 此时 JVM 会将加锁的同步范围粗化到一连串的 append 操作的外部,使其只需加一次锁就能完成
sb.append(target);
}
return sb.toString();
}
}
synchronized 的四种状态 {#synchronized-的四种状态}
- 无锁、偏向锁、轻量级锁、重量级锁。
锁膨胀方向:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
偏向锁 {#偏向锁}
减少同一线程获取锁的代价。
- 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word
的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word
的锁标记位是否为偏向锁以及当前线程 Id
是否等于 Mark Word
的 ThreadID
即可,省去了大量有关锁申请的操作。
简单点来说就是:当一个线程访问同步块并获取锁时会在对象头和栈帧中的锁记录中存储锁偏向的线程id,以后该线程在进入和退出同步块时不需要进行 CAS
(Compare And Swap)操作加锁和解锁,从而提高程序运行性能。
不适用于锁竞争比较激烈的多线程场合。
轻量级锁 {#轻量级锁}
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适用的场景:线程交替执行同步块。
若出现同一时间多个线程竞争同一把锁的情况,就会导致轻量级锁膨胀为重量级锁。
在讲解轻量级锁的加锁和解锁过程,有必要先了解下锁的内存语义:
- 当线程释放锁时,Java 内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
- 当线程获取锁时,Java 内存模型会把该线程对应的本地内存置为无效,使得被监视器保护的临界区代码必须从主内存中读取共享变量。
轻量级锁加锁过程:
-
在代码进入同步块的时候,如果同步对象锁状态为无锁状态时(锁标志位为
01
状态),虚拟机首先将会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word
的拷贝,官方称之为Displaced Mark Word
。这时候线程堆栈与对象头的状态如图所示: -
拷贝对象头中的
MarkWord
到锁记录中; -
拷贝成功后,虚拟机将使用 CAS 操作尝试将对象头中的
Mark Word
更新为指向Lock Record
的指针,并将Lock Record
里的owner
指针指向object mark word
(即锁对象的Mark Word
)。如果更新成功,则执行步骤 4,否则执行步骤 5。 -
如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且锁对象头中的
Mark Word
的锁标志位设置为00
,表示此对象正处于轻量级锁锁定的状态,这时候线程堆栈与对象头的状态如图所示: -
如果这个更新操作失败了,虚拟机会先检查锁对象头中的
Mark Word
是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行。否则说明存在多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为10
,Mark Word
中存储的将会是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态,与此同时当前线程会尝试使用自旋来获取锁,自旋咱们前面讲过,就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁解锁过程:
- 通过 CAS 操作尝试把线程中复制的
Displaced Mark Word
对象替换当前锁对象的MarkWord
; - 如果替换成功,整个同步过程就完成了;
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;
偏向锁、轻量级锁、重量级锁汇总 {#偏向锁轻量级锁重量级锁汇总}
synchronized 和 ReentrantLock 的区别 {#synchronized-和-reentrantlock-的区别}
在 Java 5 之前 synchronized
是仅有的同步手段,在 Java 5 开始又提供了 ReentrantLock
(再入锁)的实现,它的语义和 synchronized
基本相同,不过可以通过代码调用其 lock()
方法来获取锁,使代码编写更加灵活。
-
位于
java.util.concurrent.locks
包,即JUC
包; -
和
CountDownLatch
、FutureTask
、Semaphore
一样基于AQS
实现。查看
ReentrantLock
的源码:点进
acquire()
方法。acquire()
和acquireQueued()
两个方法都位于AbstractQueuedSynchronizer
抽象类中(队列同步器,简称AQS
),它是 Java 并发构建锁或其它同步组件的基础框架,是 JUC 包的核心,一般使用AQS
的主要方式是继承,使用其提供的抽象方法来管理同步状态。 -
能够实现比
synchronized
更细粒度的控制,如控制fairness
(公平); -
调用
lock()
之后,必须调用unlock()
释放锁; -
性能未必比
synchronized
高,但也是可重入的。synchronized
在经过一系列优化后,在低竞争的场景下性能可能优于ReentrantLock
。
ReentrantLock 公平性的设置:
// 参数为 true 时,倾向于将锁赋予等待时间最久的线程
// 公平性是减少线程饥饿情况的一种方法。
ReentrantLock fairLock = new ReentrantLock(true);
-
公平锁:获取锁的顺序按照先后调用
lock()
方法的顺序(慎用);通用场景中公平性未必有那么重要,Java 默认的调度策略很少会导致饥饿情况的发生,若要保证公平性则会引入额外的开销,导致吞吐量的下降。
因此建议只有当程序确实有公平性需要的时候才将其设置为true
。 -
非公平锁:抢占的顺序不一定,看运气;
-
synchronized
是非公平锁。
举个 ReentrantLock
公平性例子:
public class ReentrantLockDemo implements Runnable {
// 设置为公平锁
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread thread1 = new Thread(rtld);
Thread thread2 = new Thread(rtld);
thread1.start();
thread2.start();
}
/* 输出:
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
*/
}
从输出结果看,Thread-0
和 Thread-1
是交替执行的,确实保证了公平。
再将其改成非公平锁试试看,fair
参数值改为 false
。
public class ReentrantLockDemo implements Runnable {
// 设置为非公平锁
private static ReentrantLock lock = new ReentrantLock(false);
@Override
public void run() {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread thread1 = new Thread(rtld);
Thread thread2 = new Thread(rtld);
thread1.start();
thread2.start();
}
/* 输出:
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-0 get lock
Thread-1 get lock
Thread-1 get lock
Thread-1 get lock
Thread-1 get lock
*/
}
一直是 Thread-0
获取到锁在执行,不公平,执行很多次后才切换为执行 Thread-1
。
实际使用 ReentrantLock
中,也是以上面这个例子的习惯使用,每个 lock()
操作对应一个 try catch finally
并在 finally
中调用 unlock()
方法确保锁被释放。
ReentrantLock
将锁对象化 ,可以同普通对象那样使用,因此能够利用它做出精细的同步操作,实现 synchronized
难以实现的用例,比如:
- 判断是否有线程,或者某个特定线程在排队等待获取锁;
- 带超时的获取锁的尝试;
- 感知有没有成功获取锁。
总结:
-
synchronized
是关键字,ReentrantLock
是类; -
ReentrantLock
可以对获取锁的等待时间进行设置,避免死锁; -
ReentrantLock
可以获取各种锁的信息; -
ReentrantLock
可以灵活地实现多路通知; -
机制:
synchronized
操作对象头的Mark Word
,ReentrantLock.lock()
方法调用Unsafe
类的park()
方法。以下源码版本为
JDK 11.0.2
,查看ReentrantLock
的源码:点进
acquire()
方法。点进
acquireQueued()
方法。再点进
parkAndCheckInterrupt()
方法。继续点。
最终发现调用的是
Unsafe
类的park
方法。Unsafe
类是一个类似后门的工具,可以用来在任意内存地址位置读写数据。
什么是 Java 内存模型中的 happens-before? {#什么是-java-内存模型中的-happens-before}
Java 内存模型 JMM {#java-内存模型-jmm}
- Java内存模型(即 Java Memory Model,简称 JMM)是一种抽象概念并不真实存在,它描述一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JVM 运行程序的实体是线程,每个线程在被创建时 JVM 都会为其创建一个工作内存(也叫栈空间)用于存储线程私有数据。
JAVA 内存模型(JMM)规定所有变量都要存放在主内存中,但主内存是共享的,因此线程对主内存中的共享变量不能直接操作,需要将要操作的主内存中的共享变量拷贝一份到自己的工作内存,操作完毕后再写回主内存。
每个线程的工作内存是私有的,不同的线程无法访问对方的工作内存,所以必须通过主内存来完成线程间的通信(即传值)。
JMM 中的主内存:
- 存储 Java 实例对象;
- 包括成员变量、类信息、常量、静态变量等;
- 属于数据共享区域,多线程并发操作时会引发线程安全问题。
JMM 中的工作内存:
- 存储当前方法的所有本地变量信息,本地变量对其他线程不可见;
- 还存有字节码行号指示器、Native 方法信息;
- 属于线程私有数据区域,不存在线程安全问题。
JMM 与 Java 内存区域划分是不同的概念层次。
- JMM 描述的是一组规则,围绕原子性,有序性、可见性展开,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式;
- 唯一相似点:都存在共享区域和私有区域;主内存属于共享区域,从某种程度上说应该包含堆和方法区,而工作内存属于私有区域,从某种程度上说应该包括程序计数器、虚拟机栈、本地方法栈。
主内存与工作内存的数据存储类型以及操作方式归纳:
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中;
- 引用类型的本地变量:引用存储在工作内存的栈帧中,实例则存储在主内存中;
- 成员变量、
static
变量、类信息均会被存储在主内存中; - 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。
JMM 如何解决可见性问题? {#jmm-如何解决可见性问题}
简单来说先把数据加载到缓存寄存器,运算结束后写回内存。
存在一种情况:某个 CPU 对某个共享变量的修改可能只是体现在该内核的缓存中,这是个本地状态,而运行在其它内核上的线程加载的可能还是旧状态,这种情况有很大概率导致一致性的问题。从理论上说,多线程共享引入了复杂的数据依赖性,不管编译器,处理器怎么做重排序都必须尊重数据依赖性的要求,否则就会打破数据的正确性,这也是 JMM 要解决的问题。
在执行程序时,为了提高性能,处理器和编译器往往会对要执行的指令重排序,但不能随意重排序,指令重排序需要满足以下条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序。
即:无法通过 happens-before
原则推导出来的,才能进行指令的重排序。
JVM 内部实现重排序通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性的保证,即实现了各种 happens-before
的规则。它的复杂度在于需要尽量确保各种编译器,各种体系结构的处理器能够提供一致的行为。
内存屏障(Memory Barrier)是一个 CPU 指令,作用有 2 个:
- 保证特定操作的执行顺序;
- 保证某些变量的内存可见性。
happens-before 原则 {#happens-before-原则}
A 操作的结果需要对 B 操作可见,则 A 与 B 存在 happens-before
关系。
happens-before 原则是判断数据是否存在竞争,线程是否安全的主要依据;依靠此原则可以解决在并发环境下两个操作之间存在冲突的问题。
举个例子:
i = 1; // 线程 A 执行
j = i; // 线程 B 执行
假设线程 A happens-before
线程 B,即线程 A 先于线程 B 执行,那么就可以确定线程 B 执行完毕后 j == 1
是成立的,如果线程 A 和 线程 B 之间不存在 happens-before
原则,则 j == 1
不一定成立,这就是 happens-beforea
原则的威力。
happens-before
的八大原则:
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
即一段代码在单线程中的执行结果是有序的。注意:只是执行结果有序。编译器,处理器会对指令重排序但不影响最终执行的结果。
这个规则只对单线程有效,多线程环境下无法保证准确性。
-
锁定规则:一个
unLock
操作先行发生于后面对同一个锁的lock
操作;无论是单线程还是多线程环境,一个锁处于被锁定状态,必须先执行
unLock
操作后才能执行lock
操作。 -
volatile
变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;这是一条比较重要的规则。标志着
volatile
保证了线程的可见性,通俗点说就是如果某个线程先去写一个volatile
变量,另一个线程读取这个volatile
变量,这个写操作一定是happens-before
读操作。 -
传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;
-
线程启动规则:
Thread
对象的start()
方法先行发生于此线程的每一个动作; -
线程中断规则:对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生;假设线程 A 调用线程 B 的
interrupt()
方法,线程 B 中断标识会被设置,线程 B 也能够感知到此变化。 -
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过
Thread.join()
方法结束,Thread.isAlive()
的返回值手段检测线程是否已终止执行;假定线程 A 在执行过程中通过调用线程 B 的
join()
方法等待线程 B 执行完毕,则在线程 B 执行完毕时对共享变量的修改对于线程 A 来说是可见的。 -
对象终结规则:一个对象的初始化完成先行发生于它的
finalize()
方法的开始;一个对象的构造函数必须在其
finalize()
方法被调用前执行完,这保证了finalize()
方法执行时该对象的所有Field
字段值都是可见的。
happens-before
的概念:
- 如果两个操作不满足上述任意一个
happens-before
规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序; - 如果操作 A
happens-before
操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的。
通过一个例子加深下印象:
private int value = 0;
// 线程 A 执行 write 操作,且优先于线程 B 执行
public void write (int input) {
value = input;
}
// 线程 B 执行 read 操作
public int read () {
return value;
}
那么线程 B 获得结果是什么呢?
就以这段代码依次分析其 happens-before
八大原则,显而易见 5、6、7、8 这四个规则与这段代码毫无关系。
由于 write
和 read
分属两个线程,对于程序次序规则要求需要在一个线程内,因此不适用。
这个两个方法并没有用到锁,因此锁定规则也不适用。
变量 value
并没有 volatile
关键字修饰,因此 volatile
变量规则也不适用。
最后对于传递规则也很明显不适用这段代码。
综合以上分析无法通过 happens-before
原则推导出线程 A happens-before
线程 B,因此即便可以确定线程 A 优先于线程 B 执行,但是也无法确定线程 B 执行的结果是什么,所以得出的结论是:这段代码不是线程安全的。
想要修复这段代码使其线程安全,可以使其满足锁定规则,即:
private int value = 0;
// 线程 A 执行 write 操作,且优先于线程 B 执行
public synchronized void write (int input) {
value = input;
}
// 线程 B 执行 read 操作
public synchronized int read () {
return value;
}
或者满足 volatile
变量规则,即:
private volatile int value = 0;
// 线程 A 执行 write 操作,且优先于线程 B 执行
public void write (int input) {
value = input;
}
// 线程 B 执行 read 操作
public int read () {
return value;
}
volatile 关键字 {#volatile-关键字}
volatile
:JVM 提供的轻量级同步机制。
volatile 作用 {#volatile-作用}
-
保证被
volatile
修饰的共享变量对所有线程总是可见的;即一个线程修改了
volatile
修饰的变量,其它线程可以立即感知到这个变化的值。 -
禁止指令重排序优化。
volatile的可见性
被 volatile
修饰的变量对所有线程总是立即可见的,对 volatile
变量的写操作总是能立即反应到其它线程中,但对 volatile
变量的运算操作在多线程环境中并不保证安全性。
举个例子:
public class VolatileVisibility {
public static volatile int value = 0;
public static void increase() {
value++;
}
}
value
变量的任何改变都会立马反应到其它线程中。
但在多线程同时执行 increase()
方法时,会出现线程安全问题。因为 ++
操作并不是原子性操作,它分为两个步骤:首先读取 value
的值,再写回一个新值,即在原来的基础上 +1
;如果在读取旧值和写回新值期间别的线程有读取到了旧值,那么这两个线程就会在同样的旧值上进行 +1
操作,最终 value
的值只会增加 1,从而引发线程安全问题。因此必须要使用 synchronized
关键字修饰 increase()
方法,即:
public class VolatileVisibility {
public static volatile int value = 0;
public synchronized static void increase() {
value++;
}
}
synchronized
还会创建内存屏障指令,会强制将所有 CPU 中的结果刷新到主存中,保证操作结果的内存可见性,同时也会使得先获得锁的线程的所有操作都 happens-before
随后获得此锁的线程的操作,因此 synchronized
也具备与 volatile
相同的特性:可见性。所以在此例子中完全可省去 volatile
,即:
public class VolatileVisibility {
public static int value = 0;
public synchronized static void increase() {
value++;
}
}
再来看一个使用 volatile
也能达到线程安全的例子:
public class VolatileSafe {
volatile boolean shutdown;
public void close() {
shutdown = true;
}
public void doWork() {
while (!shutdown) {
System.out.println("safe....");
}
}
}
由于对 boolean
类型的变量 shutdown
的修改属于原子操作,因此可以使用 volatile
修饰该变量,使其修改可以被其它线程立即可见,达到线程安全。
volatile 变量为何立即可见? {#volatile-变量为何立即可见}
- 当写一个
volatile
变量时,JMM 会把该线程对应的工作内存中的共享变量值刷新到主内存中; - 当读取一个
volatile
变量时,JMM 会把该线程对应的工作内存置为无效,只能从主内存中重新读取共享变量。
volatile 如何禁止重排优化? {#volatile-如何禁止重排优化}
- 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化;
- 强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
来看一个经典的单例的双重检测实现例子,也是面试时经常要写的实现线程安全的单例写法:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检测
if (instance == null) {
// 同步
synchronized (Singleton.class) {
if (instance == null) {
// 多线程环境下可能会出现问题的地方
instance = new Singleton();
}
}
}
return instance;
}
}
这个例子可能会出现的问题是:某个线程在执行到第一次检测时读取到的 instance
不为 null
,但此时 instance
可能还未完成初始化,因为 new Singleton()
分为以下 3 步执行:
memory = allocate(); // 1. 分配对象内存空间
instance(memory); // 2. 初始化对象
instance = memory; // 3. 设置 instance 指向刚分配的内存地址,此时 instance != null
但是在这 3 个步骤中可能会发生重排序,导致实际的执行顺序是这样的:
memory = allocate(); // 1. 分配对象内存空间
instance = memory; // 3. 设置 instance 指向刚分配的内存地址,此时 instance != null,但是对象还没有完成初始化!
instance(memory); // 2. 初始化对象
可以这样重排序是因为步骤 2 和步骤 3 没有数据的依赖关系,且重排序后不影响在单线程中程序执行的结果。
但重排序只保证单线程中语义的一致性,不保证多线程环境下的一致性,因此当某个线程访问 instance
不为 null
时,此时 instance
未必完成初始化,从而引发线程安全问题。
解决的方法是使用 volatile
修饰 instance
变量禁止指令重排序即可:
public class Singleton {
// 禁止指令重排优化
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检测
if (instance == null) {
// 同步
synchronized (Singleton.class) {
if (instance == null) {
// 多线程环境下可能会出现问题的地方
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 和 synchronized 的区别 {#volatile-和-synchronized-的区别}
volatile
本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止;volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法和类级别;volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量修改的可见性和原子性;volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞;volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化。
CAS(Compare and Swap) {#cascompare-and-swap}
synchronized
这种独占锁属于悲观锁,始终假定会发生并发冲突,因此会屏蔽一切可能影响到数据完整性的操作;除此之外还有乐观锁,它假设不会发生并发冲突,所以只在提交数据时检查是否违反数据完整性,如果提交失败则会重试,乐观锁最常见的就是 CAS。
CAS
是一种高效实现线程安全性的方法。
-
支持原子更新操作,适用于计数器,序列发生器等场景;
序列发生器:给变量自增的工具。
-
属于乐观锁机制,号称
lock-free
,只是上层感知的无锁,底层还是有加锁行为的; -
CAS操作失败时由开发者决定是继续尝试,还是执行别的操作。
CAS思想:包含三个操作数 ------ 内存位置(V)、预期原值(A)和新值(B)
执行 CAS 操作时,将内存位置(V)与预期原值(A)进行比较,如果匹配,处理器自动将该位置的值更新为新值否则处理器不做任何操作。
CAS 多数情况下对开发者来说是透明的,并不需要我们利用 CAS 代码实现线程安全。
-
JUC
的atomic
包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选;如:
java.util.concurrent.atomic.AtomicInteger
类,它的getAndIncrement()
方法调用了Unsafe
类的getAndAddInt()
方法,利用VALUE
字段的内存地址偏移直接完成操作。因为
getAndAddInt()
方法需要返回数值,添加了失败重试的逻辑,即while
循环。 -
Unsafe
类虽提供 CAS 服务,但因能够操纵任意内存地址读写而有隐患; -
Java 9 以后,可以使用
Variable Handle API
来替代Unsafe
。
缺点:
-
若循环时间长,则开销很大,如
getAndAddInt()
方法中失败重试的while
循环; -
只能保证一个共享变量的原子操作;
-
ABA 问题
如内存位置(V)初始读取的值时 A,且在准备赋值时检测其值仍为 A,但是在赋值前的这段时间中其值被改为了 B 然后又被改为了 A,这种情况会让 CAS 操作误认为它的值从未改变,这个漏洞就称之为 CAS 操作的ABA问题。
解决:
AtomicStampedReference
,通过控制变量值的版本保证 CAS 的正确性。因此在使用 CAS 前要考虑清楚 ABA 问题是否影响程序并发的正确性,如果需要解决 ABA 问题则需要改用传统的互斥同步可能会比原子类更高效。
Java 线程池 {#java-线程池}
频繁的创建和销毁线程很浪费系统资源,Java 线程池就是用来解决此问题的,它能够重复利用线程,提升效率。
利用 Executors创建不同的线程池满足不同场景的需求 {#利用-executors创建不同的线程池满足不同场景的需求}
-
newFixedThreadPool(intnThreads)
:指定工作线程数量的线程池; -
newCachedThreadPool()
:处理大量短时间工作任务的线程池;- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
- 如果线程闲置的时间超过间值(一般为60秒)则会被终止并移出缓存;
- 所以系统长时间闲置的时候,不会消耗什么资源。
-
newSingleThreadExecutor()
:创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它;可保证顺序执行各个任务,在任意时间只会有一个活动的线程。
-
newSingleThreadScheduledExecutor()
与newScheduledThreadPool(intcorePoolSize)
:定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程; -
newWorkStealingPool()
内部会构建ForkJoinPool
,利用working-stealing
算法,并行地处理任务,但不保证处理顺序。Work-Stealing
算法:某个线程从其他队列里窃取任务来执行。
Fork/Join框架
- 把大任务分割成若干小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架;
Fork/Join
框架是ExecutorService
的一种具体实现,目的是为了更好利用多处理器。为能递归拆分成子任务的工作类型量身设计,使用所有可用的运算能力提升应用性能,与Map-Reduce
原理一致。
Fork/Join
框架将任务分发给线程池中的工作线程,使用工作窃取 working-stealing
算法。
Fork/Join
将大任务拆分成若干个子任务,这些子任务会被放置到不同的队列中,并为每个队列创建单独的线程执行队列中任务。由于各个线程完成任务的时间不会完全相同,此时已完成任务的空闲线程会从其它处于 busy
状态的工作线程窃取等待执行的任务,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部取任务执行,而窃取任务线程永远从双端队列的尾部取任务执行。
为什么要使用线程池?
-
降低资源消耗;
通过重复利用已创建的线程来降低线程创建和销毁带来的消耗。
-
提高线程的可管理性;
线程是稀缺资源,无限制的创建不仅会消耗系统资源还会降低系统的稳定性,使用线程池可以进行统一的分配,调优、监控。
讲了这么多,来看看 Executors
的源码:
newFixedThreadPool()
、newCachedThreadPool()
、newSingleThreadExecutor()
这 3 个方法返回都是 ThreadPoolExecutor
对象。
ThreadPoolExecutor
类继承 AbstractExecutorService
类,AbstractExecutorService
实现了 ExecutorService
接口, ExecutorService
最终继承 Executor
接口。
newSingleThreadScheduledExecutor()
、newScheduledThreadPool(intcorePoolSize)
、newWorkStealingPool()
返回的对象虽各不相同,但向上追溯最终继承 Executor
接口。
所以它们都是属于 Executor
框架体系下的类或接口。
Executor 框架 {#executor-框架}
根据一组执行策略实现调度,执行和控制的异步任务框架,提供任务提交与任务运行分离开的机制。
JUC 的三个 Executor
接口:
-
Executor
:运行新任务的简单接口,将任务提交和任务执行细节解耦;Executor
接口只有一个execute()
方法。对于不同的
Executor
接口的实现,其execute()
方法表现的也不一致,可能是创建一个新线程立即启动、也可能是使用已有的工作线程运行传入的任务、还可能是根据设置线程池或阻塞队列的容量决定将传入的任务放入阻塞队列中或拒绝接收传入的任务。// 使用 Thread 类创建线程并启动的伪代码 Thread t = new Thread(); t.start(); // 使用 Executor 启动线程执行任务的伪代码,它无需调用 start() 方法 Thread t = new Thread(); executor.execute(t);
-
ExecutorService
:具备管理执行器和任务生命周期的方法,提交任务机制更完善;如返回
Future
的submit()
方法。 -
ScheduledExecutorService
:支持Future
和定期执行任务。
Java 标准库中提供了上述三个接口的几种基础实现,如 ThreadPoolExecutor
,ScheduledThreadPoolExecutor
。
它们的设计特点是:高度的可调节性和灵活性,尽量满足复杂多变的实际应用场景。
Executors
类则从简化使用的角度为我们提供了各种方便的静态工厂方法。
分析最基础最重要的 ThreadPoolExecutor
下图描述的是应用从提交任务 → 线程池处理 → 线程池内部处理任务 → 任务处理完成返回数据给应用的流程。
-
线程池会有一个工作队列来接收提交的任务;
从
Executors
源码中可以看到这个工作队列可以是容量为零的SynchronousQueue
,也可以是
LinkedBlockingQueue
。 -
工作队列接收到任务后就会排队将任务提交给线程池(即工作线程的集合),该集合需要在运行中管理线程的创建和销毁,如当任务执行压力较大时创建新的工作线程执行任务,当任务量较小时结束闲置一段时间的工作线程。
线程池的工作线程被抽象为静态内部类
Worker
,线程池维护的其实就是一组Worker
对象。其中
firstTask
属性用来保存传入的任务;thread
属性实在构造函数中使用getThreadFactory()
创建出的线程。Worker
实现了Runnable
接口,在启动时会调用其run()
方法执行传入的任务。 -
在上面的源码中能看到
ThreadFactory
提供线程池所需的创建线程的逻辑; -
若任务提交被拒绝,如线程池处于
shutdown
状态,新来的线程根据RejectedExecutionHandler
接口实现的机制来处理,Java 标准库中提供了ThreadPoolExecutor.AbortPolicy
默认实现,也可以自己实现RejectedExecutionHandler
接口根据实际情况进行自定义处理。
ThreadPoolExecutor 的构造函数 {#threadpoolexecutor-的构造函数}
-
corePoolSize
:核心线程数量;可大致理解为长期驻留的线程数量,对于不同的线程池这个值会有很大的区别。
如
Executors.newFixedThreadPool()
可手动传入,而
Executors.newCachedThreadPool()
则默认为 0。 -
maximumPoolSize
:线程不够用时能够创建的最大线程数;如
Executors.newFixedThreadPool()
可手动传入,而
Executors.newCachedThreadPool()
则默认为Integer
的最大值,说明可以动态的创建很多很多很多的线程。 -
workQueue
:任务等待队列;当任务提交时,若线程池中的线程数量大于等于
corePoolSize
值时,会将该任务封装成Worker
对象放入等待队列中。由于存在许多不同的队列,使用不同的队列会有不同的排队机制,这就增加了灵活性。 -
keepAliveTime
:允许的空闲时间。当线程池中的线程数量大于
corePoolSize
值时,线程的空闲时间大于keepAliveTime
设置的时间时会将其销毁。 -
threadFactory
:创建新线程,默认使用Executors.defaultThreadFactory()
方法创建新线程;使用默认的
Executors.defaultThreadFactory()
创建新线程会使创建的线程具有相同的优先级,且为非守护线程,另外还设置了线程的名称。 -
handler
:线程池的饱和策略。如果等待队列满了,且没有空闲的线程,此时再提交的新任务就需要一种策略来处理。
可以实现
RejectedExecutionHandler
接口根据实际业务需求自定义策略handler
。默认提供四种可选策略。
AbortPolicy
:直接抛出异常,这是默认策略;CallerRunsPolicy
:用调用者所在的线程来执行任务;DiscardOldestPolicy
:丢弃队列中靠最前的任务,并执行当前任务;DiscardPolicy
:直接丢弃任务。
新任务提交 execute 执行后的判断 {#新任务提交-execute-执行后的判断}
- 如果运行的线程少于
corePoolSize
,则创建新线程来处理任务,即使线程池中的其它线程是空闲的; - 如果线程池中的线程数量大于等于
corePoolSize
小于maximumPoolSize
,则只有当workQueue
满时才创建新的线程去处理任务; - 如果设置的
corePoolSize
和maximumPoolSize
相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue
未满,则将请求放入workQueue
中,等待有空闲的线程去从workQueue
中取任务并处理; - 如果运行的线程数量大于等于
maximumPoolSize
,这时如果workQueue
已经满了,则通过handler
所指定的策略来处理任务;
ThreadPoolExecutor 中一种优雅的传值方式 {#threadpoolexecutor-中一种优雅的传值方式}
线程池和线程一样也会有生命周期,通过状态值来表现;另外线程池是用来管理线程的,对线程的数量肯定是要了如指掌,所以在 ThreadPoolExecutor
中很巧妙的将状态值和有效线程数合二为一,并将其存储到 AtomicInteger
类型的 ctl
变量中。
ctl
变量的高 3 位用来保存运行状态(runState
),剩余的低 29 位用来保存活动线程数量(workerCount
)。
runStateOf()
方法用来获取运行状态;workerCountOf()
方法用来获取活动线程数;ctlOf()
方法用来获取前两者;这三个方法用的都是 &
、|
、~
操作,执行起来很高效很优雅。
线程池的状态 {#线程池的状态}
-
RUNNING
:能接受新提交的任务,并且也能处理阻塞队列中的任务; -
SHUTDOWN
:不再接受新提交的任务,但可以处理存量任务;在线程池处于
RUNNING
状态时调用shutdown()
方法会使线程池变为SHUTDOWN
状态。在调用
finalize()
方法执行过程中也会调用shutdown()
方法使线程池变为SHUTDOWN
状态。 -
STOP
:不再接受新提交的任务,也不处理存量任务;会中断正在处理任务的线程。
在线程池处于
RUNNING
或SHUTDOWN
状态时调用shutdownNow()
方法会使线程池变为STOP
状态。 -
TIDYING
:所有的任务都已终止;正在进行最后的打扫工作,此时有效线程数
workerCount
的值为 0。线程池处于
TIDYING
状态时再调用terminated()
方法进入到TERMINATED
状态。 -
TERMINATED
:terminated()
方法执行完后进入该状态。
工作线程的生命周期 {#工作线程的生命周期}
线程池的大小如何选定 {#线程池的大小如何选定}
- CPU 密集型:线程数 = 按照核数或者核数 + 1 设定;
- I/O 密集型:线程数 = CPU 核数 * ( 1 + 平均等待时间 / 平均工作时间 )