线程创建
Thread & Runnable
Thread
和Runnable
是JDK 1.0
版本开始就出现的API
,Thread
类也实现了Runnable
接口:public class Thread implements Runnable
。
@Test
public void test1(){
Runnable task = () -> System.out.println("time:"+System.currentTimeMillis());
new Thread(task).start();
}
Thread 和 Runnable 区别:
Thread
类本身也实现了Runnable
接口,从源码来看:继承Thread
类然后重写run()
和实现Runnable
接口并传入Thread
类在实现多线程的本质上并没有区别。区别只是run()
方法内容的来源:一种是重写run()
方法;另一种是Runnable
传入。
//Thread#run
public void run() {
/*
target就是传入的Runnable,所以默认就是直接调用传入的Runnable,如果没有传入就空实现
所以,要么传入Runnable,要么重写Thread#run,否则就是空实现
*/
if (target != null) {
target.run();
}
}
但是,在实际开发中建议采用Runnable
方式:
单继承:
Thread
是类,存在单点继承问题,Runnable
是接口,就不存在这个问题;解耦:
Runnable
接口中只定义了一个run方法
,封装的是用户的工作任务,Runnable
接口将工作任务(run
方法)和线程创建/运行机制(Thread
类)进行了解耦,也更加符合面向对象设计原则中单一职责原则;
Tips:
Thread
代表的线程是与底层操作系统中的线程对应的,Thread实例
就是对底层操作系统中真实运行的线程映射,所以Thread
类内部维护着大量与操作系统交互的native方法
,同时维护着线程各种状态信息,如:name
、priority
、daemon
、state
等,Thread
可以看着是对线程执行机制的封装;
灵活性:由于工作任务和执行机制的解耦,封装工作任务的
Runnable
可以传入到多个Thread
中,从而实现一个工作任务被多个线程同时处理;而继承Thread
类方式创建线程,工作任务和线程绑定在一起,只能实现:工作任务和线程一对一关系;
Callable & Future
Thread
和Runnable
多线程编程模式存在的问题:任务执行完成之后无法获取返回结果。那如果我们想要获取返回结果该如何实现呢?从JDK 1.5
开始引入了Callable
和Future
,通过它们构建的线程,在任务执行完成后就可以获取执行结果。
@Test
public void testCallable() throws ExecutionException, InterruptedException {
Callable<Long> task = () -> {
TimeUnit.SECONDS.sleep(8);
return System.currentTimeMillis();
};
Future<Long> future = executors.submit(task);
//executors.shutdownNow();
executors.shutdown();
System.out.println("运行结果:"+future.get());
}
Callable原理
1、ExecutorService
内部submit
方法如下:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
源码分析:将Callable
包装成FutureTask
对象,FutureTask
类实现RunnableFuture
接口,该接口又继承Runnable
接口:public interface RunnableFuture<V> extends Runnable, Future<V>
,即间接的将Callable
对象封装成一个Runnable
对象。
FutureTask#run
方法内部调用Callable#call
方法,并将返回结果保存到全局变量outcome
中,同时Future
接口中定义的方法可以将该值暴露出去,大致逻辑见下图:
FutureTask
有那么一点生产者/消费者概念,Runnable
就像一个生产者,执行任务并将结果放入到变量outcome
中;Future
就像一个消费者,待生产完成,再从outcome
中获取结果:
总结
在实际开发中更多关注的是业务逻辑,而很少去关注线程执行机制,更很少去修改,所以一般采用解耦方式;再一个,实际开发中更多采用线程池创建、管理线程,工作任务一般封装到Runnable
或Callable
接口中,提交到线程池中运行。
守护线程
Java
中的线程分为两类:daemon
线程(守护线程)和user
线程(用户线程)。区别之一是当最后一个非守护线程结束时, JVM
会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM
的退出。言外之意,只要有一个用户线程还没结束, 正常情况下JVM
就不会退出。
守护线程在非守护线程全部结束后直接结束,即finally
语句块中语句也不保证一定会被执行。如下,如果thread
线程是守护线程,当所有非守护线程运行完成,thread
线程直接退出,finally
语句块也不一定会被执行到。
案例一:
Thread thread = new Thread(){
@Override
public void run() {
try {
while(!isInterrupted()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(getName()+" interrupt flag is "+isInterrupted());
interrupt();
e.printStackTrace();
}
}
}finally {
System.out.println("finally语句块被执行");
}
}
};
案例二:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("time:" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("begin sleep");
TimeUnit.SECONDS.sleep(60);
System.out.println("main thread over");
}
上面两张图对比发现:main线程
运行结束后,JVM
会自动启动一个叫作DestroyJavaVM
的线程, 该线程会等待所有用户线程结束后终止JVM进程
。
主线程结束后并不影响用户线程执行,即主线程结束后用户线程和守护线程依然可以继续执行,当所有用户线程结束后JVM
就会退出,这时守护线程也会立即结束。
注意:
1、使用Junit测试用例时,如果主线程退出,用户线程和守护线程也会立即结束,程序退出;
2、另外还需要注意:如果在
线程t1
中创建一个守护线程t2
,线程t1
执行完成也不会影响守护线程t2
的执行;3、守护线程中的
try...finally
,其中finally
语句块不一定会被执行;
线程生命周期
通用的线程生命周期可以通过五种状态来描述:初始状态、可运行状态、运行状态、阻塞状态和终止状态。具体描述见下:
初始状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于初始状态。
可运行/就绪状态:当调用了线程对象的
start方法
之后,该线程就进入了可运行状态;或者在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态;处于该状态的线程表示:一切准备工作都已完成,只需等待线程调度程序给该线程分配CPU
时间片资源执行任务。运行状态:当有空闲的
CPU
时间片资源时(操作系统任务调度),操作系统就会将其分配给一个处于可运行状态的线程,被分配到CPU
时间片的线程的状态就转成运行状态,此时开始真正运行run方法中的用户业务逻辑代码。阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行,
sleep
、join
、wait
等方法都可以导致线程阻塞。终止/死亡状态:如果一个线程的
run方法
执行结束,对于已经终止的线程,无法再使用start方法
令其进入可运行状态。
上面是在各种语言中都较为通用的线程声明周期模型描述,Java语言
则根据自身实现对其进行了简化和细化:
简化:可运行状态到运行状态是由操作系统层面调度使用,
JVM
层面不关心,也没法控制这两个状态,所以合并成一个状态统称可运行状态
;细化:阻塞状态进行了细化:
BLOCKED
、WAITING
、TIMED_WAITING
Java
线程的六种状态可参加Thread.State
:
NEW
:初始状态,线程被构建,但是还没有调用start()
方法RUNNABLE
:运行状态,Java线程把操作系统中就绪和运行两种状态统称为运行中
BLOCKED
:阻塞状态,线程等待synchronized
隐式锁时处于此状态,当获取到synchronized
隐式锁后,又会从BLOCKED
转换到RUNNABLE
状态。WAITING
:无限期等待另一个线程来执行某一特定操作的线程处于这种状态。TIMED_WAITING
:有限期/超时等待状态,超时以后自动返回。TERMINATED
:终止状态,表示当前线程已执行完毕或异常退出。
BLOCKED
、WAITING
、TIMED_WAITING
处于这三种状态之一,即使CPU
资源空闲依然没有CPU
的使用权。
阻塞API
1、等待synchronized
隐式锁的线程状态:BLOCKED
2、阻塞集合如:ArrayBlockingQueue
、LinkedBlockingQueue
,编程式锁:ReentrantLock
、ReentrantReadWriteLock
等阻塞底层都是通过LockSupport.park()
实现,而LockSupport.park()
方法又调用sun.misc.Unsafe.park()
,该方法定义如下:public native void park(boolean isAbsolute, long time)
,该种方式导致的线程阻塞线程状态为:WAITING
,void parkNanos(Object blocker, long nanos)
和void parkUntil(Object blocker, long deadline)
带超时参数阻塞的线程状态是:TIMED_WAITING
public static void main(String[] args) {
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(10);
Thread t = new Thread("simon-thread"){
@Override
public void run() {
System.out.println("begin get");
String ele = null;
try {
//Queue为空导致阻塞
ele = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ele:"+ele);
}
};
t.start();
}
查看线程栈:
3、Thread.join()
底层通过调用Object.wait()
方法实现,而Thread.join(long millis)
调用的是native void wait(long timeout)
,wait方法
有两类:void wait()
和native void wait(long timeout)
,wait()
阻塞的线程状态是WAITTING
,而wait(long timeout)
阻塞的线程状态是TIMED_WAITING
public static void main(String[] args) throws InterruptedException {
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(10);
Thread t = new Thread("simon-thread"){
@Override
public void run() {
System.out.println("begin get");
String ele = null;
try {
ele = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ele:"+ele);
}
};
t.start();
//调用join方法阻塞主线程
t.join();
}
查看线程栈:
4、Thread
类的sleep
方法最终调用的是Thread
类中native
的sleep
方法,sleep
阻塞的线程状态:TIMED_EAITING
public static void main(String[] args) throws InterruptedException {
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
Thread t = new Thread("simon-thread"){
@Override
public void run() {
System.out.println("begin get");
try {
//调用Thread.sleep()方法使线程休眠
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
查看线程栈:
5、Thread.yield()
不会释放锁,调用yield方法
线程会进入可运行状态:RUNNABLE
6、其它操作系统层的阻塞,如IO阻塞
、ServerSocket.accept()
等
public static void main(String[] args) {
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
Thread t = new Thread("simon-thread"){
@Override
public void run() {
int i = 0;
while(i != -1){
try {
//等待IO输入阻塞
i = System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
};
t.start();
}
查看线程栈:
线程终止
方式一:Thread API
方式
private Object lock = new Object();
private Integer v1 = 100;
private Integer v2 = 100;
/**
* 1、stop()会立即抛出一个ThreadDeath类型的Error,不管线程内部有没有阻塞方法;
* 2、如果线程持有某个对象锁,调用stop()终止线程后锁会被释放;
* 3、stop()容易导致状态不一致问题,如下案例,更新状态1后由于调用stop()方法导致更新状态2步骤并没有被执行,其它
* 线程访问时就会出现状态不一致问题
* */
@Test
public void testStop() throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
synchronized (lock){
System.out.println(Thread.currentThread().getName()+" 获取到锁");
try {
v1 += 10;//更新状态1
TimeUnit.SECONDS.sleep(10);//模拟耗时操作
v2 -= 10;//更新状态2
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" receive InterruptedException");
} catch (Throwable e){//ThreadDeath
System.out.println(Thread.currentThread().getName()+" receive exception:"+e);
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
synchronized (lock){
System.out.println(Thread.currentThread().getName()+" 获取到锁");
System.out.println("v1="+v1+",v2="+v2);
}
}
};
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("t1 stop");
t1.stop();
t1.join();
t2.join();
System.out.println("****finish***");
}
stop()
是一个被废弃的方法,容易导致状态不一致问题,所以在生产开发中基本不会使用。
Thread
类还提供了一个destroy()
方法,但是该方法一直没有被实现过,也被标注为废弃方法:
@Deprecated
public void destroy() {
throw new NoSuchMethodError();
}
方式二:interrupt
interrupt
本质上不会进行线程的终止操作的,它只是利用线程的中断标志位进行通知,是否真正终止线程以及何时终止都是由当前线程自己决定。主要分为三种情况:
如果目标线程在调用
Object
类wait()
、wait(timeout)
、join()
、join(timeout)
、sleep(timeout)
方法时被阻塞,那么Interrupt
会生效,该线程的中断状态将被清除,抛出InterruptedException
异常;如果目标线程是被
I/O
或者Nio
中的Channel
所阻塞,如InterruptibleChannel
相关方法、Selector
相关方法等,同样I/O
操作会被中断或者返回特殊异常值,达到终止线程目的;如果以上条件都不满足,即不含有阻塞方法,则会设置此线程的中断状态。
例子1:循环中没有阻塞方法,则可以在每次循环中根据中断状态判断是否结束:
public void testInterruptStop() throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
/**
* interrupted():static方法,等价于Thread.currentThread().isInterrupted(),但是它会清除中断状态
* Thread.currentThread().isInterrupted():不会清除中断状态,
*/
while(!Thread.interrupted()){//根据中断状态判断是否结束当前线程
System.out.println("执行");
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(5);
t1.interrupt();
t1.join();
System.out.println("****finish***");
}
例子2:循环中含有阻塞方法时,会抛出InterruptedException
异常,但是中断状态会被清除:
public void testInterruptStop() throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
/**
* interrupted():static方法,等价于Thread.currentThread().isInterrupted(),但是它会清除中断状态
* Thread.currentThread().isInterrupted():不会清除中断状态,
*/
while(!Thread.interrupted()){//判断标志位是否终止线程
System.out.println("执行");
try {
TimeUnit.SECONDS.sleep(1);//添加阻塞方法
} catch (InterruptedException e) {//阻塞方法通过抛出InterruptedException异常方式响应中断
e.printStackTrace();
//注意,如果这里通过isInterrupted()获取中断状态返回false
Thread.currentThread().interrupt();
}
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(5);
t1.interrupt();
t1.join();
System.out.println("****finish***");
}
Thread
下几个相关中断方法区别:
interrupted()
:static
方法,等价于Thread.currentThread().isInterrupted()
,但是它会清除中断状态,这个一定要注意是当前线程中断状态,与哪个Thread
实例调用没有关系,如t1.interrupted()
不是返回t1线程
中断状态;
isInterrupted()
:返回调用的Thread
实例的中断状态,如t1.interrupted()
则表示返回t1线程
中断状态,同时不会清除中断状态;
interrupt()
:触发中断;
InterruptedException
异常说明:
如果一个方法声明抛出
InterruptedException
,则该方法是阻塞方法;程序中捕获到
InterruptedException
异常时,不要e.printStackTrace()
或通过日志记录log.error("exception", e)
等方式进行简单处理,这样会导致无意间中断信号被屏蔽而得不到正常的处理。InterruptedException
异常表示接收到终止当前线程的信号,如果当前位置可以终止线程则直接进行处理;否则则需要让更上层程序感知中断发生,一般有两种处理方式:传递中断:在方法声明时添加
throws InterruptedException
;恢复中断:如果
try...catch
捕获到InterruptedException
,但是自己又没法处理,外层代码有中断标志位检测,这时就可以通过重新触发一次中断Thread.currentThread().interrupt()
,让外层的代码能够感知到中断发生过。
方式三:标志变量
在线程循环逻辑中添加一个volatile
类型标志变量判断,用来控制线程执行终止:
private volatile boolean canceled = false;
@Test
public void testFlagStop() throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
while(!canceled){//判断标志位是否终止线程
System.out.println("执行");
//位置1
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(5);
canceled = true;
t1.join();
System.out.println("****finish***");
}
自定义标志变量方式控制线程终止,注意的一种情况是:循环中存在长时间阻塞方法,比如在位置1
处添加TimeUnit.SECONDS.sleep(1000)
,即使把canceled
设置成true
,由于阻塞导致一直无法运行到循环判断处,线程也就没法终止。
所以,优先采用interrupt
通知机制,这种方式支持的更加广泛,或者interrupt
和标志变量一起:while(!cancel && !Thread.interrupted())
。
总结
实际开发中,更多采用Interrupt
通知机制实现线程的终止,而不是强制性终止线程。因为,发出停止信号的线程很可能对将要被终止的线程运行逻辑并不很熟悉,大多数情况下往往希望被终止线程完成一些列保存工作或工作交接后再停止,而不是立即终止线程导致数据处于一种混乱状态,产生数据不一致问题。
线程异常
Java 1.5
出现的UncaughtExceptionHandler
,用于当线程由于未捕获异常终止时回调接口。
首先来看个例子:
@Slf4j
public class ThreadException implements Runnable{
@Override
public void run() {
//子线程中直接抛出异常
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
try {
//子线程中抛出异常,外部线程是无法try...catch捕获到
executors.execute(new ThreadException());
log.info("主线程继续执行");
}catch (Exception e){
log.error("捕获到异常", e);
}
}
}
输出结果:
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at org.simon.core.basic.uncaughtexception.ThreadException.run(ThreadException.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
10:50:39.942 [main] INFO org.simon.core.basic.uncaughtexception.ThreadException - 主线程继续执行
从输出结果可以发现,子线程中抛出异常时主线程通过try...catch
捕获不到异常信息,可以得出一个结论:线程的异常是逃逸不出该线程环境的,当线程中抛出异常且未进行处理会导致当前线程终结,而对主线程和其它线程完全不受影响,且完全感知不到某个线程抛出的异常。
JVM
的这种设计源自于一种理念:线程是独立执行的代码片段,线程的问题应该由线程自己来解决,而不要委托到外部。基于这样的设计理念,在Java
中,线程中的异常不论是checked exception
还是unchecked exception
都应该在run()
方法边界进行try...catch
捕获并处理掉。
一旦一个线程抛出了非受检异常,JVM
就会把它杀死,在临近销毁之前,JVM
会使用Thread.getUncaughtExceptionHandler()
方法查看该线程上的UncaughtExceptionHandler
,并回调uncaughtException()
方法。
@Slf4j
public class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
/**
* UncaughtExceptionHandler#uncaughtException会在线程因未捕获异常而临近死亡时被调用
* @param t
* @param e
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("捕获到线程异常, thread name:{}, thread state:{}", t.getName(), t.getState(), e);
}
public static void main(String[] args) {
log.info("application:{}", ManagementFactory.getRuntimeMXBean().getName());
//设置DefaultUncaughtExceptionHandler,应用于所有的Thread
//如果只是指定某些线程,可以使用setUncaughtExceptionHandler
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler());
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
10, TimeUnit.SECONDS,
new ArrayBlockingQueue(10),
new ThreadFactoryBuilder().setNameFormat("executor-%d").build(),
new ThreadPoolExecutor.DiscardPolicy());
try {
executor.execute(new ThreadException());
log.info("主线程继续执行");
} catch (Exception e) {
log.error("捕获到异常", e);
}
}
}
输出结果:
11:32:16.323 [main] INFO org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - application:25160@PS20190709MKED
11:32:16.331 [main] INFO org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - 主线程继续执行
11:32:16.333 [executor-0] ERROR org.simon.core.basic.uncaughtexception.UncaughtExceptionHandler - 捕获到线程异常, thread name:executor-0, thread state:RUNNABLE
java.lang.RuntimeException: null
at org.simon.core.basic.uncaughtexception.ThreadException.run(ThreadException.java:18)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
线程executor-0
执行时抛出异常未被捕获而被终止,在线程临近死亡前会回调UncaughtExceptionHandler#uncaughtException
。
如上图,通过arthas
工具查看线程状态,可以看到线程executor-0
已被销毁掉,线程池创建了一个新线程executor-1
来代替该销毁的线程。