1 起源
- 起源:《Java高并发编程详解 多线程与架构设计》,汪文君著
- 章节:第一、二、三章
本文是前三章的笔记整顿。
2 概述
本文次要讲述了线程的生命周期、Thread
类的构造方法以及罕用API
,最初介绍了线程的敞开办法。
3 线程生命周期
3.1 五个阶段
线程生命周期能够分为五个阶段:
NEW
RUNNABLE
RUNNING
BLOCKED
TERMINATED
3.2 NEW
用new
创立一个Thread
对象时,然而并没有应用start()
启动线程,此时线程处于NEW
状态。精确地说,只是Thread
对象的状态,这就是一个一般的Java
对象。此时能够通过start()
办法进入RUNNABLE
状态。
3.3 RUNNABLE
进入RUNNABLE
状态必须调用start()
办法,这样就在JVM
中创立了一个线程。然而,线程一经创立,并不能马上被执行,线程执行与否须要听令于CPU
调度,也就是说,此时是处于可执行状态,具备执行的资格,然而并没有真正执行起来,而是在期待被调度。
RUNNABLE
状态只能意外终止或进入RUNNING
状态。
3.4 RUNNING
一旦CPU
通过轮询或其余形式从工作可执行队列中选中了线程,此时线程能力被执行,也就是处于RUNNING
状态,在该状态中,可能产生的状态转换如下:
- 进入
TERMINATED
:比方调用曾经不举荐的stop()
办法 - 进入
BLOCKED
:比方调用了sleep()
/wait()
办法,或者进行某个阻塞操作(获取锁资源、磁盘IO
等) - 进入
RUNNABLE
:CPU
工夫片到,或者线程被动调用yield()
3.5 BLOCKED
也就是阻塞状态,进入阻塞状态的起因很多,常见的如下:
- 磁盘
IO
- 网络操作
- 为了获取锁而进入阻塞操作
处于BLOCKED
状态时,可能产生的状态转换如下:
- 进入
TERMINATED
:比方调用不举荐的stop()
,或者JVM
意外死亡 - 进入
RUNNABLE
:比方休眠完结、被notify()
/nofityAll()
唤醒、获取到某个锁、阻塞过程被interrupt()
打断等
3.6 TERMINATED
TERMINATED
是线程的最终状态,进入该状态后,意味着线程的生命周期完结,比方在下列状况下会进入该状态:
- 线程运行失常完结
- 线程运行出错意外完结
JVM
意外解体,导致所有线程都强制完结
4 Thread
构造方法
4.1 构造方法
Thread
的构造方法一共有八个,这里依据命名形式分类,应用默认命名的构造方法如下:
Thread()
Thread(Runnable target)
Thread(ThreadGroup group,Runnable target)
命名线程的构造方法如下:
Thread(String name)
Thread(Runnable target,Strintg name)
Thread(ThreadGroup group,String name)
Thread(ThreadGroup group,Runnable target,String name)
Thread(ThreadGroup group,Runnable target,String name,long stackSize)
但实际上所有的构造方法最终都是调用如下公有构造方法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
在默认命名构造方法中,在源码中能够看到,默认命名其实就是Thread-X
的命令(X为数字):
public Thread() { this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);}public Thread(Runnable target) { this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);}private static synchronized int nextThreadNum() { return threadInitNumber++;}
而在命名构造方法就是自定义的名字。
另外,如果想批改线程的名字,能够调用setName()
办法,然而须要留神,处于NEW
状态的线程能力批改。
4.2 线程的父子关系
Thread
的所有构造方法都会调用如下办法:
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);
其中的一段源码截取如下:
if (name == null) { throw new NullPointerException("name cannot be null");} else { this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } }}
能够看到以后这里有一个局部变量叫parent
,并且赋值为currentThread()
,currentThread()
是一个native
办法。因为一个线程被创立时的最后状态为NEW
,因而currentThread()
代表是创立本身线程的那个线程,也就是说,论断如下:
- 一个线程的创立必定是由另一个线程实现的
- 被创立线程的父线程是创立它的线程
也就是本人创立的线程,父线程为main
线程,而main
线程由JVM
创立。
另外,Thread
的构造方法中有几个具备ThreadGroup
参数,该参数指定了线程位于哪一个ThreadGroup
,如果一个线程创立的时候没有指定ThreadGroup
,那么将会和父线程同一个ThreadGroup
。main
线程所在的ThreadGroup
称为main
。
4.3 对于stackSize
Thread
构造方法中有一个stackSize
参数,该参数指定了JVM
调配线程栈的地址空间的字节数,对平台依赖性较高,在一些平台上:
- 设置较大的值:能够使得线程内调用递归深度减少,升高
StackOverflowError
呈现的概率 - 设置较低的值:能够使得创立的线程数增多,能够推延
OutOfMemoryError
呈现的工夫
然而,在一些平台上该参数不会起任何作用。另外,如果设置为0也不会起到任何作用。
5 Thread API
5.1 sleep()
sleep()
有两个重载办法:
sleep(long mills)
sleep(long mills,int nanos)
然而在JDK1.5
后,引入了TimeUnit
,其中对sleep()
办法提供了很好的封装,倡议应用TimeUnit.XXXX.sleep()
去代替Thread.sleep()
:
TimeUnit.SECONDS.sleep(1);TimeUnit.MINUTES.sleep(3);
5.2 yield()
yield()
属于一种启发式办法,揭示CPU
调度器以后线程会被迫放弃资源,如果CPU
资源不缓和,会疏忽这种揭示。调用yield()
办法会使以后线程从RUNNING
变为RUNNABLE
状态。
对于yield()
与sleep()
的区别,区别如下:
sleep()
会导致以后线程暂停指定的工夫,没有CPU
工夫片的耗费yield()
只是对CPU
调度器的一个提醒,如果CPU
调度器没有疏忽这个提醒,会导致线程上下文的切换sleep()
会使线程短暂阻塞,在给定工夫内开释CPU
资源- 如果
yield()
失效,yield()
会使得从RUNNING
状态进入RUNNABLE
状态 sleep()
会简直百分百地实现给定工夫的休眠,然而yield()
的提醒不肯定能担保- 一个线程调用
sleep()
而另一个线程调用interrupt()
会捕捉到中断信号,而yield
则不会
5.3 setPriority()
5.3.1 优先级介绍
线程与过程相似,也有本人的优先级,实践上来说,优先级越高的线程会有优先被调度的机会,但实际上并不是如此,设置优先级与yield()
相似,也是一个揭示性质的操作:
- 对于
root
用户,会揭示操作系统想要设置的优先级别,否则会被疏忽 - 如果
CPU
比较忙,设置优先级可能会取得更多的CPU
工夫片,然而闲暇时优先级的高下简直不会有任何作用
所以,设置优先级只是很大水平上让某个线程尽可能取得比拟多的执行机会,也就是让线程本人尽可能被操作系统调度,而不是设置了高优先级就肯定优先运行,或者说优先级高的线程比优先级低的线程就肯定优先运行。
5.3.2 优先级源码剖析
设置优先级间接调用setPriority()
即可,OpenJDK 11
源码如下:
public final void setPriority(int newPriority) { this.checkAccess(); if (newPriority <= 10 && newPriority >= 1) { ThreadGroup g; if ((g = this.getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } this.setPriority0(this.priority = newPriority); } } else { throw new IllegalArgumentException(); }}
能够看到优先级处于[1,10]
之间,而且不能设置为大于以后ThreadGroup
的优先级,最初通过native
办法setPriority0
设置优先级。
个别状况下,不会对线程的优先级设置级别,默认状况下,线程的优先级为5,因为main
线程的优先级为5,而且main
为所有线程的父过程,因而默认状况下线程的优先级也是5。
5.4 interrupt()
interrupt()
是一个重要的API
,线程中断的API
有如下三个:
void interrupt()
boolean isInterrupted()
static boolean interrupted()
上面对其逐个进行剖析。
5.4.1 interrupt()
一些办法调用会使得以后线程进入阻塞状态,比方:
Object.wait()
Thread.sleep()
Thread.join()
Selector.wakeup()
而调用interrupt()
能够打断阻塞,打断阻塞并不等于线程的生命周期完结,仅仅是打断了以后线程的阻塞状态。一旦在阻塞状态下被打断,就会抛出一个InterruptedException
的异样,这个异样就像一个信号一样告诉以后线程被打断了,例子如下:
public static void main(String[] args) throws InterruptedException{ Thread thread = new Thread(()->{ try{ TimeUnit.SECONDS.sleep(10); }catch (InterruptedException e){ System.out.println("Thread is interrupted."); } }); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt();}
会输入线程被中断的信息。
5.4.2 isInterrupted()
isInterrupted()
能够判断以后线程是否被中断,仅仅是对interrupt()
标识的一个判断,并不会影响标识产生任何扭转(因为调用interrupt()
的时候会设置外部的一个叫interrupt flag
的标识),例子如下:
public static void main(String[] args) throws InterruptedException{ Thread thread = new Thread(()->{ while (true){} }); thread.start(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :"+thread.isInterrupted()); thread.interrupt(); System.out.println("Thread is interrupted :"+thread.isInterrupted());}
输入后果为:
Thread is interrupted :falseThread is interrupted :true
另一个例子如下:
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { System.out.println("In catch block thread is interrupted :" + isInterrupted()); } } } }; thread.start(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :" + thread.isInterrupted()); thread.interrupt(); TimeUnit.SECONDS.sleep(1); System.out.println("Thread is interrupted :" + thread.isInterrupted());}
输入后果:
Thread is interrupted :falseIn catch block thread is interrupted :falseThread is interrupted :false
一开始线程未被中断,后果为false
,调用中断办法后,在循环体内捕捉到了异样(信号),此时会Thread
本身会擦除interrupt
标识,将标识复位,因而捕捉到异样后输入后果也为false
。
5.4.3 interrupted()
这是一个静态方法,调用该办法会擦除掉线程的interrupt
标识,须要留神的是如果以后线程被打断了:
- 第一次调用
interrupted()
会返回true
,并且立刻擦除掉interrupt
标识 - 第二次包含当前的调用永远都会返回
false
,除非在此期间线程又一次被打断
例子如下:
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread() { @Override public void run() { while (true) { System.out.println(Thread.interrupted()); } } }; thread.setDaemon(true); thread.start(); TimeUnit.MILLISECONDS.sleep(2); thread.interrupt();}
输入(截取一部分):
falsefalsefalsetruefalsefalsefalse
能够看到其中带有一个true
,也就是interrupted()
判断到了其被中断,此时会立刻擦除中断标识,并且只有该次返回true
,前面都是false
。
对于interrupted()
与isInterrupted()
的区别,能够从源码(OpenJDK 11
)晓得:
public static boolean interrupted() { return currentThread().isInterrupted(true);}public boolean isInterrupted() { return this.isInterrupted(false);}@HotSpotIntrinsicCandidateprivate native boolean isInterrupted(boolean var1);
实际上两者都是调用同一个native
办法,其中的布尔变量示意是否擦除线程的interrupt
标识:
true
示意想要擦除,interrupted()
就是这样做的false
示意不想擦除,isInterrupted()
就是这样做的
5.5 join()
5.5.1 join()
简介
join()
与sleep()
一样,都是属于能够中断的办法,如果其余线程执行了对以后线程的interrupt
操作,也会捕捉到中断信号,并且擦除线程的interrupt
标识,join()
提供了三个API
,别离如下:
void join()
void join(long millis,int nanos)
void join(long mills)
5.5.2 例子
一个简略的例子如下:
public class Main { public static void main(String[] args) throws InterruptedException { List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList()); threads.forEach(Thread::start); for (Thread thread:threads){ thread.join(); } for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+" # "+i); shortSleep(); } } private static Thread create(int seq){ return new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+" # "+i); shortSleep(); } },String.valueOf(seq)); } private static void shortSleep(){ try{ TimeUnit.MILLISECONDS.sleep(2); }catch (InterruptedException e){ e.printStackTrace(); } }}
输入截取如下:
2 # 81 # 82 # 91 # 9main # 0main # 1main # 2main # 3main # 4
线程1和线程2交替执行,而main
线程会等到线程1和线程2执行结束后再执行。
6 线程敞开
Thread
中有一个过期的办法stop
,能够用于敞开线程,然而存在的问题是有可能不会开释monitor
的锁,因而不倡议应用该办法敞开线程。线程的敞开能够分为三类:
- 失常敞开
- 异样退出
- 假死
6.1 失常敞开
6.1.1 失常完结
线程运行完结后,就会失常退出,这是最一般的一种状况。
6.1.2 捕捉信号敞开线程
通过捕捉中断信号去敞开线程,例子如下:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { System.out.println("work..."); while(!isInterrupted()){ } System.out.println("exit..."); } }; t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.interrupt();}
始终查看interrupt
标识是否设置为true
,设置为true
则跳出循环。另一种形式是应用sleep()
:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { System.out.println("work..."); while(true){ try{ TimeUnit.MILLISECONDS.sleep(1); }catch (InterruptedException e){ break; } } System.out.println("exit..."); } }; t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.interrupt();}
6.1.3 volatile
因为interrupt
标识很有可能被擦除,或者不会调用interrupt()
办法,因而另一种办法是应用volatile
润饰一个布尔变量,并一直循环判断:
public class Main { static class MyTask extends Thread{ private volatile boolean closed = false; @Override public void run() { System.out.println("work..."); while (!closed && !isInterrupted()){ } System.out.println("exit..."); } public void close(){ this.closed = true; this.interrupt(); } } public static void main(String[] args) throws InterruptedException { MyTask t = new MyTask(); t.start(); TimeUnit.SECONDS.sleep(5); System.out.println("System will be shutdown."); t.close(); }}
6.2 异样退出
线程执行单元中是不容许抛出checked
异样的,如果在线程运行过程中须要捕捉checked
异样并且判断是否还有运行上来的必要,能够将checked
异样封装为unchecked
异样,比方RuntimeException
,抛出从而完结线程的生命周期。
6.3 假死
所谓假死就是尽管线程存在,然而却没有任何的外在体现,比方:
- 没有日志输入
- 不进行任何的作业
等等,尽管此时线程是存在的,但看起来跟死了一样,事实上是没有死的,呈现这种状况,很大可能是因为线程呈现了阻塞,或者两个线程抢夺资源呈现了死锁。
这种状况须要借助一些内部工具去判断,比方VisualVM
、jconsole
等等,找出存在问题的线程以及以后的状态,并判断是哪个办法造成了阻塞。