文章目录
- 前言
-
- [一、 变量的线程安全分析](#一、 变量的线程安全分析)
- [二、 Monitor概念](#二、 Monitor概念)
- [三、 wait notify概念](#三、 wait notify概念)
-
- [3.1 基本概念](#3.1 基本概念)
- [3.2 api介绍](#3.2 api介绍)
- [四、 wait notify正确使用方法](#四、 wait notify正确使用方法)
-
- [4.1 sleep(long n) 和 wait(long n) 区别](#4.1 sleep(long n) 和 wait(long n) 区别)
- [4.2 step 1](#4.2 step 1)
- [4.3 step2](#4.3 step2)
- [4.4 step3 - 4](#4.4 step3 - 4)
- [4.5 step5](#4.5 step5)
- [4.6 wait - notify正确模板格式](#4.6 wait - notify正确模板格式)
- 五、park&unpark
-
- [5.1 基本使用](#5.1 基本使用)
- [5.2 特点](#5.2 特点)
- [5.3 原理](#5.3 原理)
前言
本章主要整理的synchronized的原理,其中设计对象头中monitor的知识,其中,waitSet涉及wait - notify方法,然后,重点刨析了synchronized中的好几种锁对应的流程,在最后,顺便整理了一下park&unpark方法。
一、 变量的线程安全分析
1.1 成员变量与静态变量是否线程安全?
- 如果它们没有被共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够被改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全。
1.2 局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果逃离了方法的作用访问,需要考虑线程安全。
1.3 局部变量线程安全分析
- 局部变量
- 局部变量引用的对象
局部变量引用的对象是否线程安全
如果一个局部变量引用的对象没有逃离方法作用域 ,即这个对象只在当前方法内使用,且不会被其他线程访问或持有,那么它是线程安全的。
如果该对象逃离了方法作用域(例如被返回,或者作为共享数据传递给了外部),那么它可能会被多个线程访问和修改,从而导致线程安全问题。
具体举例:
1. 局部变量引用的对象没有逃离方法作用域 :
在这种情况下,对象在方法内部使用完后就消失了,因此不涉及线程安全问题。
java
class ThreadSafeLocal {
public void process() {
String str = "Hello"; // 局部变量,线程安全
str = str + " World"; // 字符串是不可变的,操作是线程安全的
System.out.println(str); // 每个线程有自己的局部副本
}
}
在这个例子中,str
是局部变量,每个线程调用 process()
时,都会有自己的 str
变量副本。并且 str
引用的 String
是不可变的,内部操作不会影响其他线程。因此,线程是安全的。
2. 局部变量引用的对象逃离了方法作用域 :
如果局部变量引用的对象被传递到方法外部,或者被多个线程共享访问,那么这个对象可能会出现线程安全问题。
java
class SharedObject {
private StringBuilder sb = new StringBuilder();
public StringBuilder getSb() {
return sb; // sb 被返回到方法外部,可能被多个线程访问
}
}
class ThreadUnsafeLocal {
public void process() {
SharedObject sharedObj = new SharedObject();
StringBuilder sb = sharedObj.getSb(); // sb 被传递到外部
sb.append(" World"); // 多线程环境下会发生竞争条件
System.out.println(sb.toString());
}
}
在这个例子中,sb
是局部变量,但它引用的 StringBuilder
对象是从 SharedObject
返回的,并且可能会被多个线程共享访问。StringBuilder
是可变的,因此多个线程同时对它进行操作时,会发生竞态条件,导致数据错误。
1.4 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说的线程安全是指,多个线程调用它们同一个示例的某个方法时,是线程安全的。也可以理解为 :
- 它们每个方法是原子的
- 但注意它们多个方法的组合不是原子的。
多个方法组合调用 :
假设我们有一个 Counter
类,它包含两个方法:increment()
和 getCount()
。increment()
会增加计数器的值,而 getCount()
会返回当前的计数值。现在我们想通过 increment()
和 getCount()
的组合来增加计数器的值并获取最新的计数。
如果没有同步机制,多个线程同时调用 increment()
和 getCount()
方法时,可能会导致结果不一致,因为这些方法的组合操作(即获取计数值并更新)并不是原子的。
java
class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子的
}
public int getCount() {
return count; // 也是线程安全的,但它只读取,不会修改
}
public void incrementAndGet() {
increment();
System.out.println(getCount()); // 方法组合不是原子的
}
}
public class ThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程,它们同时调用 incrementAndGet
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 增加计数并打印
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 增加计数并打印
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount()); // 可能不会是 2000
}
}
incrementAndGet()
:该方法组合了两个操作:首先调用 increment()
,然后调用 getCount()
。即使每个方法内部是线程安全的(getCount()
只是读取数据,没有修改),方法的组合操作仍然不是线程安全的。因为在 increment()
执行时,如果有多个线程同时调用这个组合方法,它们会竞争修改 count
的值,导致错误的最终结果。
关键点:
- 方法内部是原子操作 :每个方法(如
getCount()
)单独执行时是线程安全的。 - 多个方法的组合 :当多个方法依赖共享资源(例如
count
)并且组合执行时,没有适当的同步机制,它们的组合操作就不是原子的,容易出现竞态条件,导致线程安全问题。
不可变类线程安全
- 例如String,它在改变的时候会被重新复制 一份,不会对原来的对象进行修改,因此线程安全。
二、 Monitor概念
2.1 Java对象头
- 在32位虚拟机上 :
- 64位虚拟机则是在32位的基础上翻倍即可。
2.2 Monitor(锁)
Monitor被翻译为 监视器 和 管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向Monitor对象的指针。
- 刚开始Monitor中的Owner为null
- 当Thread2执行时synchronized(obj)就会将Monitor的所有者Owner置为Thread - 2,Monitor中只能有一个Owner。
- 在Thread - 2上锁过程中,如果 Thread - 3, Thread - 4, Thread - 5 也来执行synchronized(obj) ,就会进入 EntryList中 BLOCKED。
- Thread - 2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时候时非公平的。
- 图中WaitSet中的Thread - 0、Thread - 1是之前 获得过锁,但是条件不满足进入WAITTING状态的线程。
2.3 synchronized原理(1)
java
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter ++;
}
}
对应的字节码为 :
2.4 synchronized原理(2)
①、轻量级锁
②、锁膨胀
③、自旋优化
④、偏向锁
(1)偏向状态
(2)撤销
实现 :
java
Dog d = new Dog();
new Thread(() ->{
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (TestBiased.class) {
TestBiased.class.notify();
}
}, "t1").start();
new Thread(() ->{
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
}, "t2").start();
运行结果 :
1. 初始状态 (线程 t1
)
20:48:31.674 c.TestBiased [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
-
这是对象
d
的初始状态。 -
对象头部解释
:
- 低三位
101
:表示 无锁状态 (JVM 默认未加锁的对象会显示为101
)。 - 剩余部分:未使用,具体值根据 JVM 的实现可能是对象分代相关的标识。
- 低三位
在这一时刻,d
尚未被加锁。
2. 第一次加锁(线程 t1
)
20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
-
在
t1
中执行了synchronized (d)
,此时线程对对象d
加锁。 -
对象头部解释
:
- 对象头部分的中间位发生变化,其中存储的是 线程 ID 或 偏向锁信息。
- 偏向锁标志位:仍然显示为
101
,这表明对象处于 偏向锁状态。 - 偏向锁意味着该对象被特定的线程持有锁(
t1
持有),而未升级为轻量级锁或重量级锁。
3. 释放锁后(线程 t1
)
20:48:31.677 c.TestBiased [t1] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
- 在
t1
中锁被释放,但对象的头部没有明显变化。 - 偏向锁的特性是线程释放锁时,偏向锁状态不会立即被撤销。这是因为 JVM 试图优化加锁性能,在后续没有竞争的情况下,可以直接重新偏向到同一个线程。
4. 第二个线程初始读取状态(线程 t2
)
20:48:31.677 c.TestBiased [t2] - 00000000 00000000 00000000 00111111 10110110 11101000 00000000 00000101
- 线程
t2
唤醒后读取了对象d
的状态。 - 对象仍处于偏向锁状态,偏向锁仍然指向线程
t1
,但t2
尚未加锁。
5. 第二个线程加锁后(线程 t2
)
20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00100000 01010101 11110011 00100000 00000000 00100000
-
线程
t2
对对象d
加锁。 -
对象头部解释
:
- 偏向锁被撤销,锁升级为 轻量级锁 或 重量级锁。
- 显示了不同于偏向锁的信息,表示
t2
持有了对象的锁。 - 具体升级为轻量级锁还是重量级锁,取决于 JVM 的实现和锁竞争的激烈程度。
6. 第二个线程释放锁后(线程 t2
)
20:48:31.678 c.TestBiased [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
-
线程
t2
释放锁。 -
对象头部解释
:
- 回到了无锁状态(
101
)。 - 对象头中保存的锁相关信息被清空。
- 回到了无锁状态(
(3)批量重偏向
(4)批量撤销
⑤、锁消除
下面表示没有用锁消除优化,上面是用锁优化的情况。
三、 wait notify概念
3.1 基本概念
3.2 api介绍
- obj.wait() 让进入object监视器的线程到waitSet等待
- obj.notify() 在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll() 让object上正在waitSet等待的线程全部唤醒。
它们都是线程之间协作的手段,都属于object对象的方法,必须获得此对象的锁,才能调用这个方法 :
java
private static final Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("线程开始执行...");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其他代码...");
}
}, "t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("线程开始执行...");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其他代码...");
}
}, "t2").start();
sleep(2);
log.debug("唤醒其它线程:");
synchronized (obj) {
//obj.notify();
obj.notifyAll();
}
}
结果 :
notify 的结果 :
notifyAll 的结果 :
四、 wait notify正确使用方法
4.1 sleep(long n) 和 wait(long n) 区别
-
sleep是Thread方法,而wait是Object方法
-
sleep不需要强制和sychronized配合使用,但是wait需要和synchronized一起用
-
sleep在睡眠的同时,不会释放对象锁,但wait的时候会释放对象锁。
-
它们状态是一样的,都是TIMED_WAITTING
4.2 step 1
错误示范 :
java
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
结果 :
这种方法的问题所在。
- 是小南睡眠期间,线程阻塞,其它人都得等着。这就导致了任务运行的效率不高。
- 小南线程必须睡够两秒,就算烟提前送过来,也无法醒来
- 加了synchronized(room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main没加synchronized就好像main是翻窗户进来。
- 解决方法 : 使用wait - notify方法。
4.3 step2
只需要改成使用wait方法
java
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
java
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notifyAll();
}
}, "送烟的").start();
结果 :
- 解决了其它干活线程的阻塞的问题
- 但如果有其它线程也在等待条件呢?
4.4 step3 - 4
java
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
//room.notify();
room.notifyAll();
}
}, "送外卖的").start();
}
运行结果 :
notify :
- 此时,造成了虚假唤醒的情况,原本想要小女继续干活,结果成了唤醒小南,但是小南继续运行的条件不满足,导致了虚假唤醒
notifyAll :
- 使用notifyAll 就可以都唤醒了。小女正常了,但是会导致小南没干成活,我们在step5中继续看。
4.5 step5
java
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
改成 :
java
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果 :
- 这样被唤醒的时候,是符合被唤醒条件小女继续执行,而小南可以重新进入waitSet中等待。
4.6 wait - notify正确模板格式
java
synchronized(lock) {
while(条件判断) {
lock.wati();
}
// 干活
}
// 另一个线程
synchronized(lock) {
lock.notifyAll();
}
五、park&unpark
5.1 基本使用
它们都是LockSupport中的方法 :
java
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
park跟wait-notify类似,但是有一个重要区别,如下 :
java
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
运行结果 :
特点就是,如果在调用park方法之前调用过unpark方法,那么后续就可以恢复线程继续运行。
5.2 特点
5.3 原理
先park再unpark :
先unpark再park :