前排温馨提示:由于文章写完后篇幅较长,所以我选择了上下文的形式发布
不指定stackSize时栈溢出时方法调用深度:
public class StackSizeTest {
public static int counter = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
count();
} catch (StackOverflowError e) {
System.out.println(counter); // result -> 35473
}
}).start();
}
public static void count() {
counter++;
count();
}
}
指定stackSize为10KB
显式指定stackSize之后显著地影响了线程栈的大小,调用深度由原来的35473变成了296:
public class StackSizeTest {
public static int counter = 0;
public static void main(String[] args) {
new Thread(null,() -> {
try {
count();
} catch (StackOverflowError e) {
System.out.println(counter);
}
},"test-stack-size",10 * 1024).start(); //stackSize -> 10KB result -> 296
}
public static void count() {
counter++;
count();
}
}
通过调整局部变量大小来调整栈帧大小
要想改变栈帧的大小,通过增加局部变量即可实现。以下通过增加多个long变量(一个占8个字节),较上一次的测试,方法调用深度又有明显的减小:
public class StackSizeTest {
public static int counter = 0;
public static void main(String[] args) {
new Thread(null,() -> {
try {
count();
} catch (StackOverflowError e) {
System.out.println(counter);
}
},"test-stack-size",10 * 1024).start(); //stackSize -> 10KB result -> 65
}
public static void count() {
long a,b,c,d,e,f,g,h,j,k,l,m,n,o,p,q;
counter++;
count();
}
}
守护线程及其使用场景
通过thread.setDaemon(true)可将新建后的线程设置为守护线程,必须在线程启动前(thread.start)设置才有效。
- 守护线程的特性就是在其父线程终止时,守护线程也会跟着销毁。
- JVM只有在最后一个非守护线程终止时才会退出。
心跳检测
集群架构中,通常需要心跳检测机制。如果应用程序开一条非守护线程来做心跳检测,那么可能会出现应用主程序都终止运行了但心跳检测线程仍在工作的情况,这时JVM会因为仍有非守护线程在工作而继续占用系统的CPU、内存资源,这显然是不应该的。
下列代码简单模仿了这一场景:
public class HeartCheck {
public static void main(String[] args) {
// worker thread
new Thread(()->{
// start the heart-check thread first
Thread heartCheck = new Thread(()->{
// do interval-automatic heart check and notify the parent thread when heart check has error
while (true) {
System.out.println("do heart check");
try {
Thread.sleep(100); //interval
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
heartCheck.setDaemon(true);
heartCheck.start();
// simulate work
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
join方法详解
源码剖析
直接上源码:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
如果调用某个线程thread的join(),会分发到join(0),执行上述的第10~12行,只要当前线程获取到了CPU执行权就会轮询thread的执行状态(isAlive是个native方法,但我们能够猜到它的作用就是检测thread是否存活,即不是Terminated状态),一旦发现thread仍然存活就会释放CPU执行权(通过wait(0)的方式),等下一轮的轮询,直到thread进入终止状态,那么当前线程将从thread.join()返回。
一定要区分清楚,调用thread.join()阻塞的是当前线程,不会对thread线程造成任何影响。
join提供了一个重载的限时等待方法(这是一个经典的超时等待模型:只有当条件满足或者已超过等待时限时才返回),这也是为了避免当前线程陷入永久等待的困境,能够在等待一段时间发现目标线程仍未执行完后自动返回。
join有一个比较好玩的地方就是如果线程调用它自己的join方法,那么该线程将无限wait下去,因为:Thread.currentThread().join()会等待当前线程执行完,而当前线程正在调用当前线程的join即等当前线程执行完……就让他自个儿去慢慢玩儿吧~
join使用场景
分步骤执行任务
比如电商网站中的用户行为日志,可能需要经过聚合、筛选、分析、归类等步骤加工,最后再存入数据库。并且这些步骤的执行必须是按部就班的层层加工,那么一个步骤就必须等到上一个步骤结束后拿到结果在开始,这时就可以利用join做到这点。
下列代码简单模仿了此场景:
public class StepByStep {
public static void main(String[] args) throws InterruptedException {
Thread step1 = new Thread(() -> {
System.out.println("start capture data...");
//simulate capture data
try {
Thread.sleep(1000);
System.out.println("capture done.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
step1.start();
Thread step2 = new Thread(() -> {
try {
step1.join();
System.out.println("start screen out the data...");
Thread.sleep(1000);
System.out.println("screen out done.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
step2.start();
Thread step3 = new Thread(() -> {
try {
step2.join();
System.out.println("start analyze the data...");
Thread.sleep(1000);
System.out.println("analyze done.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
step3.start();
Thread step4 = new Thread(() -> {
try {
step3.join();
System.out.println("start classify the data");
Thread.sleep(1000);
System.out.println("classify done.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
step4.start();
step4.join();
System.out.println("write into database");
}
}
值得注意的是,如果调用未启动线程的join,将会立即返回:
public class StepByStep {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
});
t.join();
}
}
Fork/Join模型
有时任务量太大且任务是可分的(子任务之间没有上例的依赖关系),那么我们不妨将任务拆分成互不相干的子任务(这一步叫做Fork),分别为各个子任务分配一个单独线程从而实现子任务并行执行,提高执行效率,最后将个子任务的结果整合起来做最后的加工(主线程就可以使用join来等待各个子任务线程的执行结果,从而最后做一个汇总)。JDK8提供的Stream和ForkJoin框架都有此模型的身影。
异常感知
我们可以通过join的重载方法提供的限时等待,在目标任务执行时间过长时自动返回,从而采取其他弥补策略,而不至于老是傻傻地等着。
interrupt详解
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
这里有一个细节,interrupt首先会设置线程的中断标志位,然后再打断它。
查看官方文档:
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If none of the previous conditions hold then this thread's interrupt status will be set.
Interrupting a thread that is not alive need not have any effect.
由此我们可以提取三点信息:
- Timed-Waiting/Waiting中的线程被打断后首先会清除它的中断标志位,然后再抛出InterruptedException。因此被中断的线程进入
- 处于运行状态(Runnable/Running)下的线程不会被打断,但是其中断标志位会被设置,即调用它的isInterrupted将返回true
- 对终止状态下的线程调用interrupt不会产生任何效果。
isInterrupted
Tests whether this thread has been interrupted. The interrupted status of the thread is unaffected by this method.
A thread interruption ignored because a thread was not alive at the time of the interrupt will be reflected by this method returning false.
测试线程是否被中断过,该方法的调用不会改变线程的中断标志位。对一个终止状态下的线程调用过interrupt并不会导致该方法返回true。
于是我们可以使用isInterrupted来测试一下上面提取的3个结论:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t1.interrupt();
System.out.println(t1.isInterrupted()); //true
Thread.sleep(1000);
System.out.println(t1.isInterrupted()); //false
}
上述代码在t1.interrupt后马上检查t1的中断标志位,由于interrupt是先设置中断标志位,再中断,因此17行的输出检测到了中断标志位返回true;接着18~19行先等t1在抛出InterruptedException时清除标志位,再检测其中断标志位发现返回false证明了结论1:抛出InterruptedException之前会先清除其中断标志位。
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
}
});
t1.start();
t1.interrupt();
System.out.println(t1.isInterrupted()); //true
flag = false;
t1.join();
System.out.println(t1.isInterrupted()); //false
}
interrupted不会中断正在运行的线程,但会设置其中断标志位,因此第10行返回true。由第13行的输出我们还可以的处一个新的结论:对终止状态的线程调用isInterrupted始终会返回false。
interrupted
这是一个静态方法,用来检测当前线程是否被中断过,但与isInterrupted不同,它的调用会导致当前线程的中断标志位被清除且isInterrupted是实例方法。也就是说如果连续两次调用Thread.interrupted,第二次一定会返回false。
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
}
System.out.println(Thread.currentThread().isInterrupted()); //true
System.out.println(Thread.interrupted()); //true
System.out.println(Thread.interrupted()); //false
});
t1.start();
t1.interrupt();
flag = false;
}
如何优雅地终结线程
stop
Thread有一个弃用的方法stop,弃用的原因是这个方法是类似于linux中kill -9的方式强制立即终止线程,不给线程任何喘息的机会,这意味着执行了一半的程序突然没后文了,如果线程打开了I/O、数据库连接等资源时将无法及时释放他们。
利用守护线程和join
守护线程在其父线程终结时也会随之终结,因此我们可以通过将线程设置为守护线程,通过控制其父线程的终结时间来间接终结他:
public class ThreadService {
private Thread executeThread;
private volatile boolean finished;
public void execute(Runnable task) {
executeThread =new Thread(() -> {
Thread t = new Thread(() -> {
task.run();
});
t.setDaemon(true);
t.start();
try {
t.join();
finished = true;
} catch (InterruptedException e) {
System.out.println("task execution was interrupted");
}
});
executeThread.start();
}
public void shutdown(long millis) {
long base = System.currentTimeMillis();
long now = 0;
while (!finished) {
now = System.currentTimeMillis() - base;
if (now >= millis) {
System.out.println("task execution time out, kill it now");
executeThread.interrupt();
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
System.out.println("was interrupted when shutdown");
}
}
finished = true;
}
}
在上述代码中,可以通过给shutdown传入一个task执行时限,要求它在millis时间内执行完,如果超出这个时间则视为任务执行异常,通过终止其父线程来终止它。如果它执行正常,在millis时间内返回了,那也会导致父线程的结束,shutdown也能通过轮询finished状态来感知任务执行结束。
使用共享状态变量
public class ThreadCloseGraceful implements Runnable{
private volatile boolean stop = false;
<a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override
public void run() {
while (true) {
if (stop) {
break;
}
// to do here
}
}
public void shutdown() {
stop = true;
}
}</a>
这种方式的要点是,共享状态变量必须声明为volatile,这样执行线程才能及时感知到shutdown命令。
轮询中断标志位
通过轮询线程的中断标志位来感知外界的中断命令。
public class ThreadCloseGraceful extends Thread{
<a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" style="color: #25bb9b">@Override
public void run() {
while (true) {
if (Thread.interrupted()) {
break;
}
// to do here
}
}
public void shutdown() {
this.interrupt();
}
}</a>
resume和suspend
resume/suspend被弃用的主要原因是因为suspend将线程挂起时并不会释放其所持有的共享资源,如果一个线程持有一个甚至多个锁后执行suspend,那么将会导致所有等待该锁或这些锁释放的线程陷入长久的阻塞状态。如果碰巧将要resume这个被挂起线程的线程事先也有获取这些锁的需求,那么resume线程也会被阻塞,这可能导致suspend线程将无人唤醒,这些线程都将陷入永久阻塞。
因此在并发场景下,对于临界区来说,suspend和resume是线程对立的,无论是谁先进入临界区,都将导致这两个线程甚至是多个线程陷入死锁。
synchronized详解
synchronized关键字的用法:
- 如果用在实例方法上,那么线程在进入该方法(临界区)时首先要获取this对象的monitor(也就是我们通常所说的锁,术语是管程),一个monitor同一个时刻只能被一个线程持有,获取失败将陷入阻塞状态(BLOCKED),直到该锁被释放(持有锁的线程退出该方法/临界区)后该线程将加入到新一轮的锁争取之中
- 如果用在静态方法上,则需要获取当前类的Class对象的monitor,锁获取-释放逻辑和实例方法的相同。
- 用在代码块上(代码块仍然可称为临界区),前两者是JDK自身的语义,隐含着加锁的对象。而用在代码块上则需要在synchronized括号后显式指定一个同步对象,锁获取-释放逻辑依然相同
synchronized关键字的特性:
-
获取锁失败时陷入阻塞、锁释放时相应阻塞在该锁上的线程会被唤醒,这会引起线程由用户态到内核态的切换,时间开销较大,甚至大于临界区代码的实际执行开销。因此原则上要减少synchronized的使用,但是随着JDK的升级,自旋锁、适应性自旋、锁消除、锁粗化、偏向锁、轻量级锁等优化的引入(详见《深入理解Java虚拟机(第二版)》高并发章节),synchronized的开销实际上也没那么大了。
-
可重入,如果当前线程已持有某个对象的monitor,在再次进入需要该monitor的临界区时,可直接进入而无需经过锁获取这一步。
-
一个线程可同时持有多个monitor。注意,这一操作容易导致死锁的发生,以下代码就模仿了这一情景:
public class DeadLock {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
IntStream.rangeClosed(0,19).forEach(i->{
if (i % 2 == 0) {
new Thread(() -> m1()).start();
} else {
new Thread(() -> m2()).start();
}
});
}
public static void m1() {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName());
}
}
}
public static void m2() {
synchronized (lock2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName());
}
}
}
}
网友评论