文章目录
- 前言
- 一、 变量的线程安全分析
- 1.1 成员变量与静态变量是否线程安全?
- 1.2 局部变量是否线程安全?
- 1.3 局部变量线程安全分析
- 具体举例:
- 1. 局部变量引用的对象没有逃离方法作用域 :
- 2. 局部变量引用的对象逃离了方法作用域 :
- 1.4 常见线程安全类
- 多个方法组合调用 :
- 关键点:
- 不可变类线程安全
- 二、 Monitor概念
- 2.1 Java对象头
- 2.2 Monitor(锁)
- 2.3 synchronized原理(1)
- 2.4 synchronized原理(2)
- ①、轻量级锁
- ②、锁膨胀
- ③、自旋优化
- ④、偏向锁
- (1)偏向状态
- (2)撤销
- (3)批量重偏向
- (4)批量撤销
- ⑤、锁消除
- 三、 wait notify概念
- 3.1 基本概念
- 3.2 api介绍
- 四、 wait notify正确使用方法
- 4.1 sleep(long n) 和 wait(long n) 区别
- 4.2 step 1
- 4.3 step2
- 4.4 step3 - 4
- 4.5 step5
- 4.6 wait - notify正确模板格式
- 五、park&unpark
- 5.1 基本使用
- 5.2 特点
- 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();// 创建两个线程,它们同时调用 incrementAndGetThread 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 :