
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
临界资源:一次仅允许一个线程(或进程)访问和修改的共享资源,例如共享变量、文件、数据库连接等
临界区:访问临界资源的代码块
竞态条件:
synchronized 关键字、java.util.concurrent.locks.Lock 接口的实现(如 ReentrantLock)。这些机制通过互斥锁来保证同一时刻只有一个线程进入临界区java.util.concurrent.atomic 包下的类,如 AtomicInteger),它们通过 CAS(Compare-And-Swap)等底层硬件原语实现无锁并发管程(Monitor):
synchronized 关键字和 Object 类的 wait()/notify()/notifyAll() 方法就是管程机制的一种实现synchronized:
synchronized 块内,线程仍然可能发生上下文切换。但由于 synchronized 保证了互斥,即使发生上下文切换,其他线程也无法进入同一个临界区,从而保证了数据的一致性互斥和同步都可以采用 synchronized 关键字来完成
区别:
wait/notify 机制),或者等待某个结果。性能:
synchronized 或 Lock)通常会引入额外的开销(如锁的获取与释放、上下文切换等),因此相对于非线程安全的实现,其性能可能会有所下降。然而,现代 JVM 对 synchronized 进行了大量优化(如偏向锁、轻量级锁、自旋锁),在许多场景下其性能开销已经很小锁对象:理论上可以是任意的非 null 唯一对象
synchronized 是可重入的,默认是不公平的,并且在竞争激烈时会升级为重量级锁
原则上:
final 对象this 作为锁对象,锁住的 this 正好是当前实例,从而保护了该实例的共享成员类名.class 字节码对象作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住整个类而不是某个实例同步代码块格式:
synchronized(锁对象){
// 访问共享资源的核心代码
}实例:
public class Demo {
static int counter = 0;
//static 修饰,则元素是属于类本身的,不属于对象,与类一起加载一次,只有一个
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问
synchronized 修饰的方法的同步性不具备继承性。这意味着子类重写父类的 synchronized 方法时,如果子类方法没有显式加 synchronized 关键字,它将不再是同步方法。如果子类方法也被 synchronized 修饰,那么它们各自的锁对象取决于方法是实例方法还是静态方法,通常不是“同一把锁”,除非它们锁的是同一个 Class 对象或 this 实例
用法:直接给方法加上一个修饰符 synchronized
// 同步实例方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
// 同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}同步方法底层也是有锁对象的:
如果方法是实例方法:同步方法默认用 this 作为锁对象
public synchronized void test() {}
// 等价于
public void test() {
synchronized(this) {}
}如果方法是静态方法:同步方法默认用 类名.class 作为锁对象
class Test{
public synchronized static void test() {}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {}
}
}线程八锁是 Java 并发编程中一个经典的面试题和学习点,主要用于考察开发者对 synchronized 关键字锁定的对象类型及其作用范围的理解。核心在于辨别不同的 synchronized 用法(实例方法、静态方法、同步代码块)分别锁定了哪个对象。
核心思想:判断多个线程尝试获取的锁是否是同一个对象。如果是同一个对象,它们之间就会互斥(串行执行);如果不是同一个对象,它们可以并发执行。
synchronized 修饰静态方法时,或者同步代码块使用 synchronized(ClassName.class) 时,锁定的是该类的 Class 对象。由于一个类在 JVM 中只有一个 Class 对象,因此所有线程(无论创建了多少个实例)对该类的静态同步方法或 ClassName.class 同步块的访问都会被同一把锁控制,从而实现线程安全。synchronized 修饰实例方法时,或者同步代码块使用 synchronized(this) 时,锁定的是当前方法所属的实例对象。这意味着只有对同一个实例对象的并发访问才会受到这把锁的控制。如果存在多个实例对象,每个实例都有自己的锁,它们之间互不影响,可以并发执行。线程不安全:线程 1 调用的是静态同步方法 a(),它锁定了 Number.class 对象。线程 2 调用的是实例同步方法 b(),它锁定了 n2 实例对象(即 this)。由于 Number.class 和 n2 是两个完全不同的锁对象,它们之间不会发生互斥,因此可以并发执行。
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}线程安全:线程 1 调用的是静态同步方法 a(),它锁定了 Number.class 对象。线程 2 调用的是静态同步方法 b(),它也锁定了 Number.class 对象。由于它们都尝试获取同一个 Class 对象锁,因此它们之间会发生互斥,保证了串行执行。
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象。Monitor 是 JVM 底层(通常是 C++)实现的一个同步原语,其数据结构实例存储在堆内存中。当 synchronized 给一个对象上锁,并且锁升级到重量级锁状态时,该对象头的 Mark Word 中就会被设置指向这个 Monitor 对象的指针。
|-------------------------------------------------------|---------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|---------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|---------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|---------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|---------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|---------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|---------------------||----------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|----------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|----------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|----------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|----------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|----------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|----------------------------------------------------------------------|--------------------|工作流程(重量级锁):
初始状态:Monitor 中 Owner 字段为 null。
获取锁:当 Thread-2 执行 synchronized(obj) 并成功获取锁时,Monitor 的所有者 Owner 字段被置为 Thread-2。一个 Monitor 只能有一个 Owner。obj 对象的 Mark Word 会被更新,指向该 Monitor 对象的地址,同时锁标志位变为 10(表示重量级锁)。对象原有的 Mark Word(如果是从轻量级锁膨胀而来,则原 Mark Word 已在线程栈的锁记录中;如果是直接获取重量级锁,则 Mark Word 直接更新)会被妥善处理,以便解锁时恢复。

竞争锁:在 Thread-2 持有锁期间,如果 Thread-3、Thread-4、Thread-5 也尝试执行 synchronized(obj),它们会进入 Monitor 的 EntryList(一个由 BLOCKED 线程组成的双向链表),等待获取锁。
释放锁:Thread-2 执行完同步代码块的内容,会释放锁。它会根据 obj 对象头中 Monitor 地址找到对应的 Monitor,将 Owner 字段设置回 null。同时,obj 对象的 Mark Word 会被恢复到其解锁前的状态(例如,01 表示无锁状态,或者重新进入可偏向状态等)。
唤醒竞争者:释放锁后,Monitor 会唤醒 EntryList 中等待的线程来竞争锁。这个竞争是非公平的,如果这时有新的线程(不来自 EntryList)想要获取锁,它可能直接抢占到,导致 EntryList 中的线程继续阻塞。
WaitSet:WaitSet 中的 Thread-0,是以前获得过锁,但因为在同步块内调用了 obj.wait() 方法,条件不满足而主动释放锁并进入 WAITING 状态的线程。它们需要通过 obj.notify() 或 obj.notifyAll() 才能被唤醒,唤醒后会尝试重新竞争锁。

注意:
synchronized 必须是进入同一个对象的 Monitor 才有上述的互斥效果。synchronized 的对象不会关联监视器,自然也就不受 Monitor 机制的互斥保护。synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
偏向锁是 synchronized 的一种优化,旨在提高在没有并发竞争的情况下获取锁的性能。其思想是“偏向”于第一个获取锁的线程,该线程之后再次获取该锁时,不再需要进行任何同步操作。
101(偏向锁状态),并使用 CAS 操作将获取锁的线程 ID 记录到 Mark Word。
一个对象创建时:
0x05(即最后 3 位为 101),thread、epoch、age 都为 0。-XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟开启偏向锁的原因是:在程序刚启动时,通常会有许多线程进行初始化操作,如果一开始就启用偏向锁,频繁的偏向撤销反而会降低效率。hashCode(),就再也无法进入偏向状态了。这是因为 hashCode 值需要存储在 Mark Word 中,而偏向锁也需要 Mark Word 空间来存储线程 ID,两者会发生冲突。-XX:-UseBiasedLocking 可以禁用偏向锁。需要注意的是,从 JDK 15 开始,偏向锁默认是禁用的,因为在现代多核 CPU 和高并发场景下,偏向锁带来的收益不明显,反而可能增加 JVM 的复杂性。撤销偏向锁的状态:
hashCode():如前所述,由于 Mark Word 冲突,偏向锁会被撤销。wait()/notify():wait() 方法要求线程必须持有重量级锁并释放它,因此偏向锁会直接升级为重量级锁。批量撤销:
当一个对象有多个线程要加锁,但加锁的时间是错开的(即短时间内的交替竞争,没有同时竞争),可以使用轻量级锁来优化。轻量级锁对使用者是透明的(不可见)。
可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块。synchronized 是可重入的,这意味着一个线程可以多次获取同一个锁而不会死锁。可重入锁的主要作用是简化了并发编程模型,避免了因递归调用或内部方法调用而导致的死锁。
轻量级锁在没有竞争时(如锁重入),每次重入仍然需要执行 CAS 操作。偏向锁正是为了进一步优化这种完全无竞争的场景,避免了重入时的 CAS 开销。
锁重入实例:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2(); // 线程再次尝试获取 obj 锁,因为是可重入的,所以不会阻塞
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}轻量级锁原理:
创建锁记录(Lock Record)对象:
每个线程的栈帧都会包含一个锁记录的结构。当线程第一次进入同步块时,会在其栈帧中创建一个 Lock Record,用于存储锁定对象的 Mark Word 的副本。

尝试获取锁:
CAS 替换成功:
如果 CAS 替换成功,对象头中存储了 Lock Record 地址和状态 00(轻量级锁)。这表示由该线程成功给对象加锁。

如果 CAS 失败,有两种情况:
synchronized 锁重入:线程会在其栈帧中添加一个新的 Lock Record,其 Displaced Mark Word 字段为 null,作为重入的计数标记。
当退出 synchronized 代码块(解锁时)
Displaced Mark Word 为 null:表示这是一次重入的解锁。此时只需重置该 Lock Record,表示重入计数减 1。Displaced Mark Word 不为 null:表示这是最外层锁的解锁。这时使用 CAS 将 Lock Record 中保存的 Mark Word 值恢复给对象头。 当在尝试加轻量级锁的过程中,CAS 操作无法成功,且不是锁重入(即有其它线程正在持有该对象的轻量级锁,存在真正的并发竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

Thread-1 加轻量级锁失败,进入锁膨胀流程:
10。
当 Thread-0 退出同步块解锁时,尝试使用 CAS 将其 Lock Record 中保存的 Mark Word 值恢复给对象头。由于 Mark Word 已经被修改为指向 Monitor 的地址,CAS 操作会失败。这时 Thread-0 会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,并唤醒 EntryList 中 BLOCKED 的线程(包括 Thread-1)。
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋来进行优化。自旋是指线程在循环中不断尝试获取锁,而不是立即进入阻塞状态。
注意:
特点:
自旋锁情况:
自旋成功的情况:

自旋失败的情况:

自旋锁说明:
锁消除是指对于被 JVM 即时编译器(JIT)检测出不可能存在竞争的共享数据的锁进行消除。这是 JVM 运行时的一种优化技术。
锁消除主要是通过逃逸分析来支持。如果堆上的共享数据不可能“逃逸”出去被其它线程访问到(即它只在当前线程内部使用,是线程局部的),那么就可以把它们当成私有数据对待,也就可以将它们相关的锁操作进行消除(同步消除)。
当 JVM 探测到一串连续的操作都对同一个对象加锁时,它可能会将这些细粒度的加锁操作合并,把加锁的范围扩展(粗化)到整个操作序列的外部,从而减少频繁的锁获取和释放带来的性能损耗。
一些看起来没有加锁的代码,其实隐式的加了很多锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}String 是一个不可变的类。在 JDK 1.5 之前,+ 操作符进行字符串拼接通常会转化为 StringBuffer 对象的连续 append() 操作。StringBuffer 的每个 append() 方法都是同步的(因为它内部使用了 synchronized)。
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}在这种情况下,如果 sb 对象没有逃逸到方法外部,JIT 编译器会发现对 sb 的多次 synchronized(this) 操作实际上都是对同一个局部对象进行的,且没有其他线程可以访问 sb。因此,JIT 可能会将这些连续的锁操作粗化,扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以,甚至直接进行锁消除。
多把不相干的锁:例如,一间大屋子有两个功能:睡觉、学习,它们互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低,因为两个人必须排队。
将锁的粒度细分(Lock Granularity),即使用多把锁来保护不同的独立资源,可以提高并发度:
解决方法:为不同的独立共享资源准备多个对象锁。
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> { bigRoom.study(); }).start();
new Thread(() -> { bigRoom.sleep(); }).start();
}
class BigRoom {
private final Object studyRoom = new Object();
private final Object sleepRoom = new Object();
public void sleep() throws InterruptedException {
synchronized (sleepRoom) {
System.out.println("sleeping 2 小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom) {
System.out.println("study 1 小时");
Thread.sleep(1000);
}
}
}死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止
Java 死锁产生的四个必要条件:
四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失
public class Dead {
public static Object resources1 = new Object();
public static Object resources2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 线程 1:占用资源 1,请求资源 2
synchronized(resources1){
System.out.println("线程 1 已经占用了资源 1,开始请求资源 2");
Thread.sleep(2000);//休息两秒,防止线程 1 直接运行完成。
//2 秒内线程 2 肯定可以锁住资源 2
synchronized (resources2){
System.out.println("线程 1 已经占用了资源 2");
}
}
}).start();
new Thread(() -> {
// 线程 2:占用资源 2,请求资源 1
synchronized(resources2){
System.out.println("线程 2 已经占用了资源 2,开始请求资源 1");
Thread.sleep(2000);
synchronized (resources1){
System.out.println("线程 2 已经占用了资源 1");
}
}
}).start();
}
}定位死锁的方法:
jstack <PID> 定位死锁。jstack 会打印出所有线程的堆栈信息,如果存在死锁,它会自动检测并报告。找到死锁的线程后,查看其堆栈信息和锁的持有/等待情况,可以定位到源码位置进行解决优化。top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp <PID> 来定位是哪个线程,最后再用 jstack <PID> 的输出来看各个线程栈resources1,再尝试获取 resources2jconsole 或 jvisualvm 等图形化工具,它们通常提供死锁检测功能,可以直观地展示死锁情况活锁:指的是任务或者执行者没有被阻塞,但由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程,从而无法向前推进。线程虽然没有阻塞,但却无法完成有效工作。它与死锁的区别在于,死锁是线程永远停止活动,而活锁是线程一直在活动但没有进展。
两个线程互相改变对方的结束条件,最后谁也无法结束:
class TestLiveLock {
static volatile int count = 10;
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
Thread.sleep(200);
count--;
System.out.println("线程一 count:" + count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
Thread.sleep(200);
count++;
System.out.println("线程二 count:"+ count);
}
}, "t2").start();
}
}饥饿:一个线程由于优先级太低,或者总是被其他高优先级线程抢占 CPU 资源,导致它长时间得不到 CPU 调度执行,也无法完成其任务。虽然理论上它最终会运行,但等待时间过长,以至于实际上无法完成其任务,就像被“饿死”了一样。
线程必须首先获取到锁对象的监视器(即持有锁),然后才能调用 锁对象.wait()、锁对象.notify() 或 锁对象.notifyAll() 方法。
Object 类 API:
// 唤醒正在等待对象监视器的单个线程(具体唤醒哪个线程是不确定的)
public final void notify()
// 唤醒正在等待对象监视器的所有线程
public final void notifyAll()
// 导致当前线程等待,直到另一个线程调用该对象的 notify() 或 notifyAll() 方法,或者被中断
public final native void wait()
// 有时限的等待,在指定毫秒数后自动结束等待,或者被唤醒、被中断
public final native void wait(long timeout)
// 更精细地有时限等待,本质上还是调用了 wait(long timeout),只是做了一个四舍五入
public final void wait(long timeout, int nanos)说明:
wait() 方法会使当前线程放弃对象锁,并进入该对象的等待队列 WaitSet,状态变为 WAITING 或 TIMED_WAITING(如果设置了超时)notify() 或 notifyAll() 方法用于从 WaitSet 中唤醒线程。被唤醒的线程并不会立即执行,它们需要重新竞争对象锁,只有成功获取到锁后才能从 wait() 方法处继续执行提示
阻塞(Blocked):是在“等门开”,门一开(锁释放),阻塞的线程会自动参与竞争。
等待/挂起(Waiting):是在“进屋睡觉”,除非别人过来推醒(notify/unpark)它,否则它永远不会去竞争锁。
对比 sleep():
sleep() 方法属于 Thread 类,用于控制线程自身的暂停wait() 方法属于 Object 类,用于线程间的协作通信sleep() 方法时,线程不会释放它持有的任何对象锁,但会释放 CPU 执行权wait() 方法时,线程会立即释放它持有的对象锁,并进入 WaitSet,同时释放 CPU 执行权wait()、notify() 和 notifyAll() 方法必须在同步代码块或同步方法中使用,即在获取到对象锁之后才能调用,否则会抛出 IllegalMonitorStateExceptionsleep() 方法可以在任何地方使用底层原理:
obj.wait() 方法,它会释放 obj 的锁,并进入 obj 的 WaitSet,状态变为 WAITING。notify() 或 notifyAll() 时被唤醒。被唤醒后,它们会从 WaitSet 转移到 EntryList,重新竞争对象锁。只有成功获取锁后,才能继续执行。
虚假唤醒:
notify() 或 notifyAll() 调用,或者其等待的条件并未满足的情况下被唤醒。虽然 Java 规范允许虚假唤醒发生,但实际 JVM 实现中并不常见。然而,为了程序的健壮性,我们必须始终防范虚假唤醒。解决方案:
while 循环而非 if 判断等待条件:这是解决虚假唤醒和多条件等待问题的关键。当线程从 wait() 返回时,它必须再次检查条件是否满足。如果使用 if,线程只检查一次,一旦被唤醒,即使条件仍不满足也可能继续执行,导致逻辑错误。而 while 循环确保线程只有在条件真正满足时才继续执行。notifyAll() 替代 notify():当有多个线程在等待不同的条件,或者有多个线程在等待同一个条件时,notify() 只能随机唤醒一个线程。如果被唤醒的线程发现条件仍不满足,它会再次 wait(),而真正等待条件满足的线程可能仍然在睡眠。notifyAll() 可以唤醒所有等待线程,让它们重新检查自己的条件,从而避免“死锁”或“饥饿”问题(即本应被唤醒的线程没有被唤醒)。@Slf4j
public class DemoWaitNotify {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false; // 有没有外卖
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
// 使用 while 循环检查条件,防止虚假唤醒和条件不满足时继续执行
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("外卖送到没?[{}]", hasTakeout);
// 使用 while 循环检查条件,防止虚假唤醒和条件不满足时继续执行
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
// 假设是送烟的,先送烟
hasCigarette = true;
log.debug("送烟的:烟到了噢!");
room.notifyAll(); // 唤醒所有等待 room 锁的线程
// 模拟一段时间后送外卖
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
hasTakeout = true;
log.debug("送外卖的:外卖到了噢!");
room.notifyAll(); // 再次唤醒所有等待 room 锁的线程
}
}, "送货员").start();
}
}LockSupport 是用来创建锁和其他同步类的线程原语,它提供了更底层、更灵活的线程阻塞和唤醒机制。
LockSupport 类方法:
LockSupport.park():暂停当前线程。 LockSupport.unpark(Thread thread):为指定线程 thread 颁发一个许可。 thread 正在 park() 阻塞,则它会被唤醒thread 没有阻塞,那么许可会在下次 park() 时立即被消耗public class ParkUnparkDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start..."); // 1
try {
// 模拟一些工作,确保 unpark 有机会先执行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " park..."); // 2
LockSupport.park(); // 线程在此处暂停,直到获得许可
System.out.println(Thread.currentThread().getName() + " resume..."); // 4
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(2); // 确保 t1 已经执行到 park() 或者 unpark() 已经先发放了许可
System.out.println("unpark..."); // 3
LockSupport.unpark(t1); // 给线程 t1 发放许可
}
}LockSupport 出现就是为了增强 wait & notify 的功能,解决其一些局限性:
wait()、notify() 和 notifyAll() 必须配合 Object 的监视器(synchronized 锁)一起使用,并且只能在同步块中调用park()、unpark() 不需要持有任何对象锁park() 和 unpark() 以线程为单位来阻塞和唤醒特定线程,可以精确控制notify() 只能随机唤醒一个等待线程,notifyAll() 是唤醒所有等待线程park() 和 unpark() 采用许可机制,unpark() 可以先于 park() 执行,这相当于预先发放了一个许可,当线程稍后调用 park() 时,会立即消耗这个许可并继续运行,而不会阻塞wait() 和 notify() 则不能先 notify(),否则 notify() 会失效,因为没有线程在等待park()/unpark() 就像一个可以预存一个产品的仓库 unpark() 可以把产品存进去,消费者 park() 来取时直接取走park() 发现没有产品就等待,生产者 unpark() 来了再放进去并通知wait() 会释放它所关联的对象锁资源并进入等待队列park() 不会释放当前线程可能持有的任何对象锁,它只负责阻塞当前线程,但会释放 CPU成员变量和静态变量:
局部变量:
线程安全类示例:String、Integer、StringBuffer、Random、ThreadLocalRandom、Vector、Hashtable、以及 java.util.concurrent 包下的所有类(如 ConcurrentHashMap、CopyOnWriteArrayList 等)。
定义:一个类被称为线程安全的,通常是指多个线程调用它们同一个实例的某个方法时,该方法内部的操作是线程安全的。
组合非原子性:需要特别注意,即使类本身是线程安全的,多个方法的组合操作可能不是原子的,仍然需要额外的同步机制来保证组合操作的线程安全。
Hashtable table = new Hashtable();
// 假设有两个线程同时执行这段代码
// 线程 1 检查 if(table.get("key") == null) 为真
// 线程 2 检查 if(table.get("key") == null) 也为真
// 线程 1 执行 table.put("key", value1)
// 线程 2 执行 table.put("key", value2)
// 结果:value1 可能被 value2 覆盖,或者反之,导致数据不一致。
if(table.get("key") == null) { // get() 是线程安全的,但 if 判断和 put() 之间不是原子的
table.put("key", value); // put() 也是线程安全的
}
// 正确做法:将整个组合操作放在一个同步块中
synchronized (table) {
if (table.get("key") == null) {
table.put("key", value);
}
}无状态类线程安全:如果一个类没有任何成员变量(即没有状态),那么它自然是线程安全的,因为没有共享数据可供竞争。
不可变类线程安全:String、Integer 等都是不可变类。这意味着它们内部的状态一旦创建就不能改变。因此,它们的方法不会修改自身状态,也不会引入竞态条件,所以是线程安全的。
// String 的 replace()、substring() 等方法,底层是新建一个对象来存储结果,而不是修改原对象
Map<String, Object> map = new HashMap<>(); // 线程不安全 (HashMap 不是同步的)
String S1 = "..."; // 线程安全 (String 是不可变的)
final String S2 = "..."; // 线程安全 (String 是不可变的,final 只是保证引用不变)
Date D1 = new Date(); // 线程不安全 (Date 是可变的,其内部状态可以被修改)
final Date D2 = new Date(); // 线程不安全 (final 保证 D2 引用不能变,但 D2 引用的 Date 对象内容可以变)外星方法:指在一个同步块或临界区内部调用了外部方法(通常是用户提供的回调、抽象方法或多态方法),而这个外部方法的行为是不可控的。这种调用可能导致:
public abstract void foo(Student s); 如果在同步方法中调用 foo(),而 foo() 的具体实现是未知的,就可能引入上述风险。应尽量避免在持有锁的情况下调用外星方法。
评论