共计 9457 个字符,预计需要花费 24 分钟才能阅读完成。
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 :false
Thread 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 :false
In catch block thread is interrupted :false
Thread 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();}
输入(截取一部分):
false
false
false
true
false
false
false
能够看到其中带有一个 true
,也就是interrupted()
判断到了其被中断,此时会立刻擦除中断标识,并且只有该次返回true
,前面都是false
。
对于 interrupted()
与isInterrupted()
的区别,能够从源码(OpenJDK 11
)晓得:
public static boolean interrupted() {return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {return this.isInterrupted(false);
}
@HotSpotIntrinsicCandidate
private 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 # 8
1 # 8
2 # 9
1 # 9
main # 0
main # 1
main # 2
main # 3
main # 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
等等,找出存在问题的线程以及以后的状态,并判断是哪个办法造成了阻塞。