当前位置: 首页 > news >正文

JUC并发编程07 - wait-ify/park-un/安全分析

wait-ify

基本使用

需要获取对象锁后才可以调用 锁对象.wait(),notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU

Object 类 API:

public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒

说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁

想象一下:你和朋友约好去吃饭,但你说:“我还没到,你先等等我。”等你到了,你就说:“我到了,可以吃饭了!”——这就是 线程间的通信,而 waitnotify 就是这种“等我”、“我好了”的对话工具。

  • wait():我还没准备好,我先“挂起”(暂停),你先干别的。
  • notify():我搞定了,叫醒一个在等的人。
  • notifyAll():我搞定了,所有人别等了,都醒醒抢活干!

对比 sleep():

  • 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
  • 锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
  • 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用

sleep()wait()虽然都能让线程“暂停”,但目的、用法、行为完全不同。

第一点:原理不同

  • Thread.sleep():是线程自己控制自己,“我想歇2秒”。
  • Object.wait():是线程之间互相配合,“我没准备好,等你通知我”。
方法场景
sleep()你做饭做到一半,说:“我先躺2分钟再炒菜” → 你占着灶台,别人不能用
wait()你发现没食材,就对室友说:“我等你买菜回来叫我” → 你让出厨房,室友可以进去放东西、做饭

总结一下:sleep 是自我管理,wait 是协作沟通。sleep() 是线程的“个人行为”,wait() 是多个线程之间的“团队协作”。

// sleep 属于 Thread 类 → Thread.sleep()
Thread.sleep(1000);// wait 属于 Object 类 → 必须某个对象调用 obj.wait()
synchronized (lock) {lock.wait();
}

第二点:对锁的处理机制不同

方法是否释放锁?是否释放 CPU?
sleep()不释放释放
wait()释放释放

也就是说:

  • sleep():占着茅坑不拉屎(有锁不干活)
  • wait():主动让位,让别人进来改数据
Object lock = new Object();// 线程A:使用 sleep —— 不释放锁
new Thread(() -> {synchronized (lock) {System.out.println("A 拿到锁,开始 sleep...");try {Thread.sleep(5000); // 睡5秒,但锁不放!} catch (InterruptedException e) {}System.out.println("A sleep 结束");}
}, "A").start();// 线程B:想进 lock,但必须等 A 出来
new Thread(() -> {synchronized (lock) {System.out.println("B 终于拿到锁了!");}
}, "B").start();

代码输出如下:

A 拿到锁,开始 sleep...
(5秒后)
A sleep 结束
B 终于拿到锁了!

B 等了整整5秒!因为 A 睡着也不放锁!换成 wait()

Object lock = new Object();// 线程C:使用 wait —— 释放锁
new Thread(() -> {synchronized (lock) {System.out.println("C 拿到锁,发现条件不满足,开始 wait...");try {lock.wait(); // 释放锁!} catch (InterruptedException e) {}System.out.println("C 被唤醒,继续执行");}
}, "C").start();// 线程D:可以立刻拿到锁(因为C释放了)
new Thread(() -> {synchronized (lock) {System.out.println("D 拿到锁了!可以干活了!");// 干完活,叫醒 Clock.notify();}
}, "D").start();

代码输出如下:

C 拿到锁,发现条件不满足,开始 wait...
D 拿到锁了!可以干活了!
C 被唤醒,继续执行

看到了吗?C 一 wait(),D 立刻就能进!这就是释放锁的好处。如果不释放锁,别人进不来,也就没法调用 notify() 来唤醒你,就死锁了。

第三点:使用区域不同

  • wait():必须先“进门”(拿到锁),才能说“我等等”
  • sleep():在哪都能睡,站着也能打盹
  • wait() 像开会时说:“这事儿没结论,咱们下次再议” → 你得先在会议室里才能说这话
  • sleep() 像打哈欠:“我困了,眯一会儿” → 在工位、走廊都能眯
Object lock = new Object();// sleep 可以随便用
public void someMethod() {try {Thread.sleep(1000); // 没问题!} catch (InterruptedException e) {}
}// wait 不能随便用
public void badWait() {// lock.wait(); // 直接报错!没在 synchronized 里
}// wait 必须在 synchronized 里
public void goodWait() {synchronized (lock) {try {lock.wait(); // 正确:先拿锁} catch (InterruptedException e) {}}
}

如果你在 synchronized 外面调 wait(),JVM 会直接抛出:

java.lang.IllegalMonitorStateException

意思是:“你连锁都没有,凭什么说等等?”

底层原理:

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争

想象一个对象的锁(比如 synchronized(lock))是一个小屋子,门口有三个区域:

  1. Owner 区:正在屋子里干活的线程(持有锁)
  2. EntryList(入口等待区):一堆人排队等着进屋(BLOCKED 状态)
  3. WaitSet(等待区):一些人不在门口排队,而是去旁边椅子上坐着打盹(WAITING 状态)

这个“屋子”就是锁对象,比如 Object lock = new Object();

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

  • 有个线程正在屋子里干活(它是 Owner)
  • 它发现:“哎呀,条件不够,现在干不了”
  • 它说:“我先出去歇会儿,等别人告诉我好了再来”
  • 于是它走出屋子,坐到旁边的“等待椅”上(WaitSet),进入 WAITING 状态

BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片

  • BLOCKED:在门口排队的人,站着不动,不抢 CPU
  • WAITING:在休息区打盹的人,睡着了,也不抢 CPU
  • 他们都“暂停”了,操作系统不会分配时间片给他们执行

BLOCKED 线程会在 Owner 线程释放锁时唤醒

  • 一群人堵在门口(BLOCKED),等着进屋
  • 只要屋里的人(Owner)一出来(释放锁)
  • JVM 就会从队伍里选一个人进去(唤醒一个 BLOCKED 线程)

WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争

  • 打盹的人(WAITING)不会自己醒
  • 必须屋里的人喊一声:“可以干活了!”(notify()
  • 听到后,他从椅子上站起来 → 跑到门口 → 加入排队队伍(EntryList)
  • 但他要和其他人一起抢锁,不一定能马上进去!

注意:唤醒 ≠ 立刻执行!只是“可以开始抢了”,但还要排队!

代码优化

虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程

解决方法:采用 notifyAll

notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断

解决方法:用 while + wait,当条件不成立,再次 wait

@Slf4j(topic = "c.demo")
public class demo {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 (!hasCigarette) {//while防止虚假唤醒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();Thread.sleep(1000);new Thread(() -> {// 这里能不能加 synchronized (room)?synchronized (room) {hasTakeout = true;//log.debug("烟到了噢!");log.debug("外卖到了噢!");room.notifyAll();}}, "送外卖的").start();}
}

想象你在一个宿舍里:

  • 小南在等(他只认烟)
  • 小女在等外卖(她只认外卖)

现在送外卖的人来了,他喊了一声:“东西到了!”(notify())。但如果用的是 notify(),系统是随机叫一个人!可能叫到的是小南(等烟的),但来的是外卖,不是烟!小南醒了看一眼:“没烟啊?” → 但他不会再问了(因为是 if 判断),直接说:“我没活干。” 然后走了。结果:小女一直睡着,没人叫她,外卖白送了!这就是“唤醒了错误的人,正确的那个人却没醒”。

解决方法:用 notifyAll

送外卖的喊一声:“所有人给我醒醒!”(notifyAll()),小南和小女都醒来:

  • 小南一看:没烟 → 继续睡(wait()
  • 小女一看:有外卖!→ 开始吃饭,干活!

这样就能做到正确的人被唤醒。

为什么判断条件要用 while,不能用 if

if 就像:你设了个闹钟:“8点叫我起床”,闹钟响了,你睁开眼看了一眼窗外,天还没亮。你说:“哦,天没亮,我不起了。” 然后再也不看了,结果9点了你还在睡。

while 就像:闹钟响了,你看天没亮 → 继续睡;过会儿又被叫醒(别人喊你)→ 再看一眼;直到天亮了才起床。

所以:while 是“每次醒来都再检查一遍条件”,防止“醒早了”或“叫错人”。

park-un

LockSupport 是用来创建锁和其他同步类的线程原语

LockSupport 类方法:

  • LockSupport.park():暂停当前线程,挂起原语
  • LockSupport.unpark(暂停的线程对象):恢复某个线程的运行
public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("start...");	//1Thread.sleep(1000);// Thread.sleep(3000)// 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行System.out.println("park...");	//2LockSupport.park();System.out.println("resume...");//4},"t1");t1.start();Thread.sleep(2000);System.out.println("unpark...");	//3LockSupport.unpark(t1);
}

LockSupport.park() 就像按了暂停键,让某个线程“卡住不动”;

LockSupport.unpark(线程) 就像按了播放键,让那个“被卡住”的线程继续跑。

你先按“播放”(unpark),再按“暂停”(park),也不会出问题——线程照样能继续跑。

public class ParkUnparkDemo {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {System.out.println("1. start..."); // 1try {Thread.sleep(1000); // 睡1秒,让 main 线程先执行完前面的逻辑} catch (InterruptedException e) { }System.out.println("2. park..."); // 2LockSupport.park(); // 当前线程(t1)暂停!等别人叫醒我System.out.println("4. resume..."); // 4 被叫醒后才执行}, "t1");t1.start();Thread.sleep(2000); // 主线程睡2秒,确保 t1 已经执行到 park() 了System.out.println("3. unpark..."); // 3LockSupport.unpark(t1); // 唤醒 t1 线程}
}

控制台输出结果如下:

代码分析:

步骤谁干的干了啥说明
1t1打印 start...t1 开始跑了
-t1sleep(1000)睡1秒
-mainsleep(2000)main 线程也在睡
2t1打印 park...,然后 park()t1 暂停!卡在这儿不动了
3main打印 unpark...,然后 unpark(t1)main 叫醒 t1:“可以走了!”
4t1打印 resume...t1 被唤醒,继续往下走

关键点park() 会阻塞线程,直到有人调用 unpark(这个线程)

如果反过来:先 unpark,再 park 呢?

public class ParkUnparkReverse {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {System.out.println("1. start...");try {Thread.sleep(3000); // 睡3秒,确保 main 的 unpark 已经执行了} catch (InterruptedException e) { }System.out.println("2. park...");LockSupport.park(); // 此时:unpark 已经执行过了System.out.println("3. resume..."); // 这句会立刻执行吗?}, "t1");t1.start();Thread.sleep(1000);System.out.println("unpark before park!");LockSupport.unpark(t1); // 先叫醒,但 t1 还没 park// t1 继续睡,直到 3秒后执行 park()}
}

控制台输出结果如下:

重点来了:为什么 park() 没有卡住?因为 unpark(t1) 提前发了一个“许可” 给 t1。

park() 相当于:“我看看有没有许可,有就直接过,没有就等。”因为之前已经 unpark 了,所以 park() 发现“哦,有许可”,直接放行,不阻塞。

所以得出结论:unpark 可以提前发“通行许可”,park 会先检查有没有许可,有就直接通过。

为什么说它是“创建锁和其他同步类的线程原语”?

“原语”就是最基础的零件,就像乐高最小的积木块。

Java 里的 synchronizedReentrantLockAQS(AbstractQueuedSynchronizer)这些高级锁,底层都是靠 park/unpark 来实现线程的等待和唤醒。

比如:

  • 加锁失败?→ park() 当前线程,让它等着。
  • 释放锁?→ unpark() 唤醒某个等待的线程。

它们不用 wait/notify,就是因为 park/unpark 更灵活、更底层、不会因为顺序出错而卡死。

特性park/unparkwait/notify
是否需要 synchronized不需要必须配合 synchronized
是否能指定线程唤醒可以 unpark(具体线程) notify() 随机唤醒一个
先 unpark 再 park 会阻塞吗?不会,有“许可”就过不能先 notify,否则无效
更底层吗?是,JUC 包的基石是老的同步机制

LockSupport.park()unpark() 就像线程世界的“暂停键”和“播放键”,而且支持“提前播放”,是构建锁和并发工具的底层核心按钮。你可以把它理解为:操作系统级别的线程控制开关。

LockSupport 出现就是为了增强 wait & notify 的功能:

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
  • park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费
  • wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU

为什么 Java 要搞出 LockSupport.park()unpark()?它到底比老的 wait/notify 强在哪?

wait/notify 就像“广播喇叭 + 公共厕所”——你得先抢坑位(synchronized 锁),然后喊一声“我好了!”(notify),但不知道谁来接班。

park/unpark 就像“对讲机 + 私人司机”——我能点名叫醒谁就叫醒谁,不需要抢厕所,还能提前发“可以开工了”的信号。

对比维度一:是否需要“锁”才能用?

wait/notify:必须配合 synchronized(对象监视器)

synchronized (obj) {obj.wait();   // 必须先拿到 obj 的锁,否则报错!obj.notify(); // 同样要先拿到锁
}

就像你要上厕所,必须先开门进去(拿到锁),才能在里面喊“我好了!”。

park/unpark:不需要 synchronized

LockSupport.park();     // 随时可以暂停自己
LockSupport.unpark(t1); // 随时可以唤醒别人

就像你在马路上走路,突然累了,直接坐下休息(park);朋友看见了,走过来拍拍你说:“起来走啦!”(unpark),完全不用进厕所。

优势:更灵活,不依赖锁,适合做底层并发工具。

对比维度二:能不能“点名唤醒”?

wait/notify:notify 只能随机叫醒一个线程,不能指定

synchronized (obj) {obj.notify(); // 随机叫醒一个正在 wait(obj) 的线程// 我也不知道是谁!
}

就像你在公司群里发:“可以来用了!”结果三个同事都在等,到底谁来?系统随机挑一个。

park/unpark:可以精确“点名道姓”唤醒某个线程

Thread t1 = new Thread(...);
Thread t2 = new Thread(...);LockSupport.unpark(t1); // 我只想叫醒 t1
// t2 还在睡

就像你微信私聊:“小王,你来接班”,小李完全不知道这事。

优势:精准控制,避免无效唤醒,性能更高。

对比维度三:顺序能不能颠倒?先叫醒再暂停行不行?

wait/notify:不能先 notify,否则白搭!

// main 线程
synchronized (obj) {obj.notify(); // 现在没人 wait,这个 notify 就“浪费”了
}// 之后 t1 才开始 wait
Thread t1 = new Thread(() -> {synchronized (obj) {try {obj.wait(); // 等一辈子也不会被叫醒!} catch (InterruptedException e) { }}
});

就像你早上6点喊“可以吃饭了!”,结果食堂7点才开门,没人听见,白喊。

park/unpark:可以先 unpark,再 park,照样有效!

Thread t1 = new Thread(() -> {try {Thread.sleep(2000); // 先睡2秒} catch (InterruptedException e) { }System.out.println("t1 准备暂停...");LockSupport.park(); // 发现:之前已经 unpark 过我了 → 直接通过!不阻塞!System.out.println("t1 被唤醒了 or 直接通过了");
});t1.start();// 先 unpark
LockSupport.unpark(t1); // 提前发“通行许可”
System.out.println("main: 我提前叫醒 t1 了");

就像你提前给司机发微信:“等下直接走,我已经说好了。”司机来了发现绿灯亮着,直接开车走人,不用等。

优势:不会因为顺序问题导致线程永久阻塞,更安全!

对比维度四:释放资源的区别

wait:会释放锁 + 释放 CPU

synchronized (obj) {obj.wait(); // 当前线程释放 obj 的锁,其他线程可以进入 synchronized 块// 同时释放 CPU,进入等待队列
}

就像你上完厕所不仅出来了,还把门钥匙交出去了,别人能进来。

park:不会释放锁,只释放 CPU

synchronized (this) {LockSupport.park(); // 线程暂停,但仍然持有当前 synchronized 锁!// 其他线程进不来!
}

这意味着:其他线程无法进入这个 synchronized 块!就像你坐在马桶上突然发呆(park),人不动,但门还锁着,别人进不来。所以:park 一般不用在 synchronized 里!它本来就是给无锁或 AQS 这种高级玩法用的。

优势park 只负责“暂停线程”,不干涉锁逻辑,职责更单一,更适合做底层原语。

实战类比:生产者消费者模型

我们用一个“仓库放苹果”的例子对比:仓库最多放1个苹果

传统写法:wait/notify 版

Object lock = new Object();
boolean hasApple = false;// 生产者
new Thread(() -> {synchronized (lock) {if (!hasApple) {System.out.println("生产一个苹果");hasApple = true;lock.notify(); // 喊一声:有苹果了!}}
}).start();// 消费者
new Thread(() -> {synchronized (lock) {while (!hasApple) {try {lock.wait(); // 没苹果,等着} catch (InterruptedException e) { }}System.out.println("消费一个苹果");hasApple = false;}
}).start();

问题:

  • 必须 synchronized
  • notify 可能唤醒不了正确的线程(如果多个消费者)
  • 如果先 notify,消费者后 wait,就死等!

更加底层:park/unpark 版

Thread producer = null;
Thread consumer = null;// 模拟原子变量
AtomicBoolean hasApple = new AtomicBoolean(false);// 消费者线程
consumer = new Thread(() -> {if (!hasApple.get()) {System.out.println("消费者:没苹果,我先歇会...");LockSupport.park(); // 暂停自己}System.out.println("消费者:拿到苹果,开吃!");hasApple.set(false);
});// 生产者线程
producer = new Thread(() -> {System.out.println("生产者:生产一个苹果");hasApple.set(true);LockSupport.unpark(consumer); // 精准叫醒消费者
});consumer.start();
// 模拟消费者先启动并等待
Thread.sleep(1000);
producer.start();

优点:

  • 不需要 synchronized
  • 可以先 unpark(比如苹果已经放好了)
  • 精准唤醒 consumer

原理:类似生产者消费者

先 park:

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量挂起
  4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0

先 unpark:

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0

安全分析

成员变量和静态变量:

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,分两种情况:
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题

局部变量:

  • 局部变量是线程安全的
  • 局部变量引用的对象不一定线程安全(逃逸分析):
    • 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧)
    • 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用)

常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包

  • 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

  • 每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全:

Hashtable table = new Hashtable();
// 线程1,线程2
if(table.get("key") == null) {table.put("key", value);
}

无状态类线程安全,就是没有成员变量的类

不可变类线程安全:String、Integer 等都是不可变类,内部的状态不可以改变,所以方法是线程安全

  • replace 等方法底层是新建一个对象,复制过去

Map<String,Object> map = new HashMap<>();	// 线程不安全
String S1 = "...";							// 线程安全
final String S2 = "...";					// 线程安全
Date D1 = new Date();						// 线程不安全
final Date D2 = new Date();					// 线程不安全,final让D2引用的对象不能变,但对象的内容可以变

抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法:public abstract foo(Student s);

什么是线程安全?

想象你和你朋友在同一个银行账户上操作。你取钱,他也在取钱。如果系统没处理好,你们俩同时看到余额是 1000 元,你取了 500,他也取了 500,结果账户变成 -500?这就叫“线程不安全”。

线程安全就是:多个线程同时操作同一个东西时,不会出错、不会数据混乱。

成员变量和静态变量

如果它们没有共享,则线程安全

成员变量(类里的变量)和静态变量(static 变量)如果每个线程都有自己的副本,那它们就是安全的。

public class Counter {private int count = 0; // 成员变量public void add() {count++;}
}

如果每个线程都 new 一个自己的 Counter 对象:

Thread t1 = new Thread(() -> {Counter c1 = new Counter();c1.add();
});Thread t2 = new Thread(() -> {Counter c2 = new Counter(); // 和 c1 不是同一个对象c2.add();
});

这两个线程用的是不同的对象,所以 count 不共享 → 线程安全

如果它们被共享了,根据它们的状态是否能够改变,分两种情况:

如果多个线程都在用同一个对象的成员变量或静态变量,那就要看这个变量能不能被改。

如果只有读操作,则线程安全

大家都只看,不改,那没问题,就像很多人同时看一本书的内容,不会乱。

public class Config {private String version = "1.0"; // 共享的成员变量public String getVersion() {return version; // 只读}
}
  • 多个线程调用 getVersion() → 只读 → 线程安全

如果有读写操作,则这段代码是临界区,需要考虑线程安全问题

如果多个线程又读又写同一个变量,就像两个人同时改一个文档,必须上锁,否则数据会乱。

public class Counter {private int count = 0;public void add() {count++; // 读 + 写(不是原子操作!)}
}

  • 两个线程同时调用 add()
    • 线程A读到 count=5
    • 线程B也读到 count=5
    • A写入6,B也写入6 → 实际只加了一次 → 出错了
    • 这种“可能出错的区域”叫临界区,必须加锁

局部变量

局部变量是线程安全的

方法里的变量,每个线程调用时都会在自己的“栈”里创建一份,互不影响。也就是线程安全的。

public void calculate() {int temp = 10;       // 局部变量temp = temp + 5;System.out.println(temp);
}
  • 线程A调用 calculate(),有自己的 temp
  • 线程B调用,也有自己的 temp
  • 互不干扰 → 线程安全

局部变量引用的对象不一定线程安全(逃逸分析)

如果该对象没有逃离方法的作用范围,它是线程安全的(每一个方法有一个栈帧)

虽然变量是局部的,但如果它指向的对象被“放出去”了(比如返回、传给别的方法),那就可能被多个线程访问,就不安全了。但是只要这个对象只在方法内部用,用完就扔,每个线程各玩各的,就安全。

public void processData() {List<String> list = new ArrayList<>(); // 局部变量,对象也在方法内list.add("hello");list.add("world");// list 没有被返回,也没有传给别的线程
}
  • 每个线程调用这个方法,都有自己独立的 list → 安全

如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用)

如果你把这个对象“交出去”了(比如返回、存到公共地方),别人也能改它,那就可能不安全。

List<String> globalList = new ArrayList<>(); // 公共的,可能被多个线程访问public void addData() {List<String> list = new ArrayList<>();list.add("data");globalList.addAll(list); // 把局部对象的内容“暴露”出去了
}
  • 虽然 list 是局部的,但它指向的数据被加到了 globalList
  • globalList 是共享的 → 多个线程都往里加 → 可能线程不安全

这就是“逃逸”了。

常见线程安全类

String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包,这些类的设计已经考虑了多线程,你可以放心让多个线程同时用同一个实例。

String s = "hello";
Integer i = 100;
StringBuffer sb = new StringBuffer();
Vector<String> vec = new Vector<>();
Hashtable<String, String> table = new Hashtable<>();
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
  • 多个线程同时调用 sb.append("x") → 安全(StringBuffer 是同步的)
  • 多个线程同时 put 到 table → 安全(Hashtable 方法加了锁)

但注意:StringBuilder不安全!它没加锁,速度快,但多线程不能用。

线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

比如 Hashtable,你创建一个实例,10 个线程同时调用它的 put() 方法,不会出问题。Hashtable 内部加了锁,保证线程安全。

Hashtable<String, Integer> table = new Hashtable<>();// 线程1
new Thread(() -> {table.put("a", 1);
}).start();// 线程2
new Thread(() -> {table.put("b", 2);
}).start();

每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全

单个方法是安全的,但多个方法连着调用就不一定了,中间可能被别的线程插队。

Hashtable<String, Object> table = new Hashtable<>();// 线程1 和 线程2 同时执行:
if (table.get("key") == null) {table.put("key", "value"); // 问题在这里!
}
  • 线程A:get("key") → null → 进入 if
  • 线程B:get("key") → null → 也进入 if
  • A 执行 put("key", "value")
  • B 也执行 put("key", "value") → 覆盖了!而且本来只想 put 一次

getput 各自是安全的,但组合起来不是原子的

正确做法:用 putIfAbsent() 或加锁。

无状态类 & 不可变类

无状态类线程安全,就是没有成员变量的类

一个类啥变量都没有,只有一些方法,那它肯定是线程安全的,因为没有东西可以被改。

public class MathUtils {public int add(int a, int b) {return a + b; // 只用参数,没有成员变量}
}
  • 在这个例子中,多个线程调用 add() 是安全的。

不可变类线程安全:String、Integer 等都是不可变类,内部的状态不可以改变,所以方法是线程安全

不可变类:一旦创建,内容就不能改。比如 String s = "abc"; s.toUpperCase() 返回一个新字符串,原来的没变。

String s = "hello";// 线程1
new Thread(() -> {String upper = s.toUpperCase();
}).start();// 线程2
new Thread(() -> {String lower = s.toLowerCase();
}).start();

安全!因为 s 本身没变,只是生成了新对象。 replacesubstring 等方法都是这样:不改自己,新建一个对象返回

final 和线程安全

写法是否线程安全为什么
Map map = new HashMap<>()不安全HashMap 本身不是线程安全的
String S1 = "..."安全String 是不可变类
final String S2 = "..."安全final + 不可变类,更安全
Date D1 = new Date()不安全Date 是可变的,多个线程改它会乱
final Date D2 = new Date()依然不安全final 只保证 D2 不能再指向别的对象,但 Date 对象本身的内容可以被改(比如 setTime()

final 不等于线程安全!它只锁住“引用”,不锁住“对象内容”。

抽象方法与“外星方法”

抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法:public abstract foo(Student s);

“外星方法”是你控制不了的方法,比如别人重写的抽象方法,你不知道它是线程安全的还是不安全的。

public abstract class Processor {public void process(Student s) {validate(s);foo(s); // 外星方法!你不知道子类怎么实现的log(s);}public abstract void foo(Student s); // 子类自由发挥
}

写了个 process() 方法,看起来没问题;但 foo(s) 是抽象的,子类可能这样实现:

public void foo(Student s) {sharedList.add(s); // 访问共享变量,而且没加锁
}

多个线程调用process() → foo() 可能导致线程不安全。调用“外星方法”时要特别小心,尤其是在同步代码块中。

    http://www.dtcms.com/a/348883.html

    相关文章:

  • 《CF1120D Power Tree》
  • Spirng Cloud Alibaba主流组件
  • 【ElasticSearch】springboot整合es案例
  • 企业出海第一步:国际化和本地化
  • springBoot如何加载类(以atomikos框架中的事务类为例)
  • JavaScript数据结构详解
  • Docker知识点
  • 【数据分享】中国地势三级阶梯矢量数据
  • 【无标题】对六边形拓扑结构中的顶点关系、着色约束及量子隧穿机制进行严谨论述。
  • 深度剖析Spring AI源码(七):化繁为简,Spring Boot自动配置的实现之秘
  • MySQL--基础知识
  • 基础篇(下):神经网络与反向传播(程序员视角)
  • 多机多卡微调流程
  • Node.js依赖管理与install及run命令详解
  • 【文献阅读】生态恢复项目对生态系统稳定性的影响
  • CI/CD持续集成及持续交付详解
  • Jwt令牌设置介绍
  • 关于熵减 - 电子圆柱
  • feat(compliance): 添加电子商务法技术解读
  • PCB电路设计学习4 PCB图布局 PCB图布线
  • Python - 100天从新手到大师:第十五天函数应用实战
  • HTTP 接口调用工具类(OkHttp 版)
  • 如何用单张gpu跑sglang的数据并行
  • Java全栈开发面试实战:从基础到高并发场景的深度解析
  • MATLAB 与 Python 数据交互:数据导入、导出及联合分析技巧
  • `free` 内存释放函数
  • 【蓝桥杯 2024 省 C】挖矿
  • K8s 实战:六大核心控制器
  • yggjs_rlayout框架v0.1.2使用教程 01快速开始
  • python---类