问题发现
我们从一道面试题来开始进行分析。
题目内容为: 要求使用2个线程顺序输出:A1B2C3D4E5F6G7
以下为该题的简单实现:
package com.ssj.thread;
class ThreadPrint{
public static void main(String[] args) {
// 保证顺序 用来保证num线程需要等待 char线程state为0的时候才能执行
// cdl 每次countDown()的时候 state都会减去1,
// countDown方法采用cas方式保证了state操作的线程安全性
CountDownLatch cdl = new CountDownLatch(1);
String charStr = "ABCDEFG";
String numStr = "1234567";
Object lock = new Object();
new Thread(() -> {
try {
// 若是num线程先抢到执行资源 则先等待char线程输出第一个字母
// 这个必须要写在sync-lock外面保证不让num线程抢到lock
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
for (char c : numStr.toCharArray()) {
try {
// 唤醒其它线程
lock.notify();
System.out.print(c);
// 释放锁并进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 输出完后打印一个换行
System.out.println();
}
}, "num").start();
new Thread(() -> {
synchronized (lock) {
// 抢到执行资源之后 执行countDown更改cdl的状态
// 并告知在此锁上等待的线程
// 这个必须要写在sync-lock里面 保证让char线程抢到lock
cdl.countDown();
for (char c : charStr.toCharArray()) {
System.out.print(c);
try {
// 唤醒其它线程
lock.notify();
// 释放锁并进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "char").start();
}
}
我们编译并运行该方法
$ javac ThreadPrint.java
$ java -classpath ../../../ com.ssj.thread.ThreadPrint
运行结果如下:
由图所示,可以看出程序在运行结束后并没有退出,而是一直挂在那里,此时我们就需要jstack
的帮助啦
排查过程
- 首先我们先找到对应的进程号
$ jps
786 DemoOne
808 Jps
- 查看该进程里面的线程信息
$ top -Hp 786
此处省略...
804 sunsj 20 0 6542012 26900 12580 S 0.0 0.2 0:00.00 Service Thread
805 sunsj 20 0 6542012 26900 12580 S 0.0 0.2 0:00.03 VM Periodic Tas
807 sunsj 20 0 6542012 26900 12580 S 0.0 0.2 0:00.00 num
- 根据信息可以看出我们的num线程此刻还在运行,线程PID为
807
,由于jstack
的nid是16进制的,所以我们将线程号转为16进制
$ printf "%x\n" 807
327
- 找出对应堆栈信息
$ jstack 786 | grep "327" -A 10
可以看出,该线程处于WAITING
状态,分析代码可知,num
最后输出7
的时候执行了lock.wait()
而此时char
线程的循环已经执行完了却没有调用notify
方法唤起在lock
上等待的线程,导致了num
线程一直处于等待状态.
由此分析,我们在char
循环执行完之后再唤醒一次num
试试看该方法能否正常退出
更改char线程的内容如下
new Thread(() -> {
synchronized (lock) {
// 抢到执行资源之后 执行countDown更改cdl的状态
// 并告知在此锁上等待的线程
// 这个必须要写在sync-lock里面 保证让char线程抢到lock
cdl.countDown();
for (char c : charStr.toCharArray()) {
System.out.print(c);
try {
// 唤醒其它线程
lock.notify();
// 释放锁并进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 此处添加一个notify
lock.notity()
}
}, "char").start();
再次执行
$ javac ThreadPrint.java
$ java -classpath ../../../ com.ssj.thread.ThreadPrint
由图所示,程序可正常退出。至此修复完成!O(∩_∩)O
我们可以使用以下方法循环执行10000次查看效果
public static void printThread() {
// 保证本方法同步
CountDownLatch cdlFunc = new CountDownLatch(2);
// 保证顺序 用来保证num线程需要等待 char线程state为0的时候才能执行
// cdl 每次countDown()的时候 state都会减去1, countDown方法采用cas方式保证了state操作的线程安全性
CountDownLatch cdl = new CountDownLatch(1);
String charStr = "ABCDEFG";
String numStr = "1234567";
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
// 抢到lock之后 countDown cdl的状态 并通知num线程再去获取lock
// 而此时num线程已经获取不到了 只能等待char线程的nofity
cdl.countDown();
for (char c : charStr.toCharArray()) {
System.out.print(c);
try {
// 唤醒其它线程
lock.notify();
// 释放锁并进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
cdlFunc.countDown();
}, "char").start();
new Thread(() -> {
try {
// 若是num现场先抢到执行资源 则先等待让char线程去获取锁
if(cdl.getCount() > 0){
cdl.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
for (char c : numStr.toCharArray()) {
try {
// 唤醒其它线程
lock.notify();
System.out.print(c);
// 释放锁并进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 输出完后打印一个换行
System.out.println();
}
cdlFunc.countDown();
}, "num").start();
try {
cdlFunc.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
推荐一个jstack文件非常好用的的分析工具:https://gceasy.io/