文章目录
- 1. wait/notify
- 1.1 基本使用
- 1.2 优化使用
- 2. Park & Unpark
- 2.1 基本使用
- 2.2 基本原理
- 3. 线程状态
- 4. 活跃性
- 4.1 死锁
- 4.1.1 死锁介绍
- 4.2.2 死锁定位
- 4.3 活锁
- 4.4 饥饿
锁的使用,其实就是为了使用临界资源的时候来进行同步,即一个线程用完一个临界资源后,另一个线程再使用该临界资源,达到每次只有一个线程使用的目的。同时,也可以进行线程的同步,也就是使得多个线程的执行之间有顺序,比如线程1执行完set操作后,线程2再执行get操作等,这里我们就来学习一下线程的同步。
1. wait/notify
1.1 基本使用
wait/notify的特点如下:
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
- 调用wait方法后就直接加的是重量级锁
相关的方法如下:
API | 描述 |
---|---|
wait() | 无限等待 |
wait(long n) | 等待n毫秒 |
notify() | 挑选等待队列中的一个线程进行唤醒 |
notifyAll() | 将等待队列中的线程全部唤醒 |
示例代码如下:
@Slf4j(topic = "c.TestBiased")
public class Test1 {
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t2其它代码....");
}
},"t2").start();
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
代码示例如上,上面代码都锁住了同一个对象obj,然后进行了wait操作,即将t1和t2都放入了WaitSet,然后针对同一个对象锁,调用notify会挑选一个线程进行唤醒,可能是t1也可能是t2,如果调用的是notifyAll则是唤醒全部线程。
wait与sleep对比:
- 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
- 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
- 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用
1.2 优化使用
虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程。
举个例子,比如有一个线程小南和一个线程小女,这两个线程锁住的都是一个对象,如果小南线程需要一根烟才能继续,小女需要一份外卖才能继续,否则都会进入WaitSet,这时,有一个外卖员线程,外卖员带了一份外卖,如果使用notify,那么小南小女随机唤醒一个,如果唤醒的是小南,那么其烟没到,还是无法工作,这就是虚假唤醒。
解决方法:采用 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();
}
}
2. Park & Unpark
2.1 基本使用
LockSupport 是用来创建锁和其他同步类的线程原语
LockSupport 类方法:
-
LockSupport.park()
:暂停当前线程,挂起原语 -
LockSupport.unpark(暂停的线程对象)
:恢复某个线程的运行public static void main(String[] args) { Thread t1 = new Thread(() -> { System.out.println("start..."); //1 Thread.sleep(1000);// Thread.sleep(3000) // 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行 System.out.println("park..."); //2 LockSupport.park(); System.out.println("resume...");//4 },"t1"); t1.start(); Thread.sleep(2000); System.out.println("unpark..."); //3 LockSupport.unpark(t1); }
LockSupport 出现就是为了增强 wait & notify 的功能:
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
- park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费
- wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU
2.2 基本原理
每个线程都有自己的一个Park对象,由三部分组成 _counter, _cond, _mutex
,我们来就先调用park方法和先调用unpark方法分别看看park对象的变化。
-
先 park:
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
- 线程进入 _cond 条件变量挂起
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0
-
先 unpark:
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0
3. 线程状态
我们重新看下面线程的10种状态转换,可以分析出每种状态转换的情况原因:
-
NEW --> RUNNABLE
当调用
t.start()
方法时,由 NEW --> RUNNABLE -
RUNNABLE <–> WAITING
- 调用
obj.wait()
方法时,t 线程从 RUNNABLE --> WAITING - 调用
obj.notify(), obj.notifyAll(), t.interrupt()
时:- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
- 调用
-
RUNNABLE <–> WAITING
- 当前线程调用
t.join()
方法时,当前线程从 RUNNABLE --> WAITING - t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 WAITING --> RUNNABLE
- 当前线程调用
-
RUNNABLE <–> WAITING
- 当前线程调用
LockSupport.park()
方法会让当前线程从 RUNNABLE --> WAITING - 调用
LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,会让目标线程从 WAITING -->RUNNABLE
- 当前线程调用
-
RUNNABLE <–> TIMED_WAITING
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用
obj.notify(), obj.notifyAll(), t.interrupt()
时- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
-
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从TIMED_WAITING --> RUNNABLE
- 当前线程调用
-
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
Thread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
- 当前线程调用
-
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
时,当前线程从 RUNNABLE --> TIMED_WAITING - 调用
LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
- 当前线程调用
-
RUNNABLE <–> BLOCKED
- t 线程用
synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
- t 线程用
-
RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
4. 活跃性
4.1 死锁
4.1.1 死锁介绍
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止
Java 死锁产生的四个必要条件:
- 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路
四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失
4.2.2 死锁定位
- 首先命令行输入
jps
,可以打印出Java进程的所有进程ID,然后使用jstack PID
查看对应Java进程是否有死锁 - Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用
top -Hp 进程id
来定位是哪个线程,最后再用jstack <pid>
的输出来看各个线程栈 - 使用jconsole 工具检测
4.3 活锁
活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程
两个线程互相改变对方的结束条件,最后谁也无法结束,比如下面两个进程,一个进程希望count减到0结束,一个线程希望count加到20结束,由此count的值一直波动,但是始终无法波动到结束条件:
class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
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();
}
}
4.4 饥饿
饥饿: 一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束