线程简介
1. 什么是线程
现代操作系统在运行一个程序时,会为其创建一个进程。
现代操作系统调度的最小单元是线程,也叫轻量级进程。
在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
2. 为什么使用多线程
a. 更多的处理器核心
书中讲了很多,提炼一下:合理使用多核处理器,不浪费多核处理器的性能。
b. 更快的响应速度
如果有100个任务(不考虑数据一致性),多线程处理 要比 单线程一个个处理要快。
c. 更好的编程模型
Java提供了良好的多线程编程模型
3. 线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度。
线程分配到的时间片多少也就决定了线程使用处理器资源的多少;
而线程优先级就是决定线程需要多 或者 少 分配一些处理器资源的线程属性。
优先级高的线程 分配时间片的数量要多于 优先级低的线程。
Java线程中,通过整型成员变量priority来控制优先级;
优先级范围从1~10,默认线程优先级为5;数值越高,优先级越高。
注意:
a. 并不是说优先级较高的线程一定会在优先级低的线程 之前运行,优先级高指的是获得较多的运行机会。
b. 优先级高的线程会大部分会先执行,并不一定会全部执行完毕。
设置线程优先级时,针对频繁阻塞(休眠或IO操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或偏运算)的线程则设置较低的优先级。
注意:
线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不理会Java线程对于优先级的设定。
例如:
Mac OS X 10.10 JDK:1.7,无视设置的线程优先级。
Ununtu 14.04 无视设置的线程优先级。
4. 线程的状态
状态名称 说明
new 初始状态,线程被构建,但还没有调用start()方法
runnable 运行状态,Java线程将操作系统中的就绪和运行笼统的称作"运行中"
blocked 阻塞状态,表示线程阻塞于锁
waiting 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
time_waiting 超时等待状态,该状态不同于watiing,它是可以在指定的时间自行返回的。
terminated 终止状态,表示当前线程已经执行完毕。
jstack + 进程号 :可以看到对应进程的线程状态。
从上图可以看出:
a. 线程创建之后,调用start()方法开始运行。
b. 当线程执行wait()方法之后,线程进入等待状态。
c. 进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
d. 而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间达到时将会返回到运行状态。
e. 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。
f. 线程在执行Runnable()的run()方法之后将会进入到终止状态。
注意:
Java将操作系统中的运行和就绪两个状态合并称为运行状态。
5. Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序后台调度以及支持型工作。
这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。
也就是说,如果Java虚拟机中仅存在Daemon线程,而没有其他普通县城,Java虚拟机会退出。
可以通过Thread.setDaemon(true),将线程设置为Daemon线程。
注意:Daemon属性需要在启动线程之前设置。
public class DaemonThread implements Runnable {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("DaemonThread finally run");
}
}
}
}
public class Daemon {
public static void main(String[] args) {
Thread t = new Thread(new DaemonThread(), "DaemonThread");
t.setDaemon(true);
t.start();
}
}
运行结果:
main方法启动之后,紧接着就停止了。
运行Daemon程序,可以看到在Eclipse控制台没有任何输出打印。
main线程启动之后,DaemonThread线程随着main方法执行完毕而终止。而此时Java虚拟机中已经没有非Daemon线程,虚拟机退出。
Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonThread立即终止,但是DaemonThread中的finally块并没有执行。
所以构建Daemon线程时,不能依靠finally块中的程序确保资源的清理。
其实说实话,我还是没怎么理解Daemon线程是干嘛的...╮(╯▽╰)╭
在JDK官方文档中,有如下描述:
* The Java Virtual Machine exits when the only threads running are all daemon threads.
即:
当线程只剩下守护线程的时候,JVM就会推出,但是如果还有其他的任意一个用户线程还在,JVM就不会退出。
6. 启动线程
线程对象在初始化完成之后,电泳start()方法就可以启动这个线程。
线程start()方法的含义是:当前线程同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
注意:
启动一个线程前,最后为这个线程设置线程名称,方便调试定位问题。
7. 理解中断
中断可以理解为线程的一个标志位属性,他表示一个运行中的线程是否被其他线程进行了中断操作。
中断好比其他线程对该线程打了一个招呼,其他线程通过该线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,
也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
第一个Demo
public class BusyThread implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
public class SleepThread implements Runnable {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Interrupted {
public static void main(String[] args) throws Exception {
// sleepThread线程不停的睡眠
Thread sleepThread = new Thread(new SleepThread(), "sleepThread");
sleepThread.setDaemon(true);
// busyThread线程一直运行,不停止
Thread busyThread = new Thread(new BusyThread(), "busyThread");
busyThread.setDaemon(true);
// 启动两个线程
sleepThread.start();
busyThread.start();
// 休眠5秒,让两个线程充分运行
TimeUnit.SECONDS.sleep(5);
// 尝试着中断两个线程
sleepThread.interrupt();
busyThread.interrupt();
// 输出结果
System.out.println("SleepThread interrupted is : " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is : " + busyThread.isInterrupted());
TimeUnit.SECONDS.sleep(2);
}
}
输出结果:
SleepThread interrupted is : false
BusyThread interrupted is : true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.demo.concurrent.concurrent_1.SleepThread.run(SleepThread.java:11)
at java.lang.Thread.run(Thread.java:745)
我们创建两个线程,SleepThread 和 BusyThread
sleepThread线程一直睡眠,BusyThread线程一直运行
从结果可以看出:
sleepThread线程没有被中断,抛出了异常信息
BusyThread线程已经被中断。
有个疑问,为什么一直睡眠,然后中断线程会导致抛出异常?
看了下JDK API,对于interrupt()方法是这么描述的:
中断线程。
如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出SecurityException。
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,
或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,
则其中断状态将被清除,它还将收到一个 InterruptedException。
如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个ClosedByInterruptException。
如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,
就好像调用了选择器的 wakeup 方法一样。
大致意思就是:
interrupt()的作用是中断本线程。
本线程中断自己是被允许的;
其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。
如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,
或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也会让它进入阻塞状态。
若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。
例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,
但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。
中断一个“已终止的线程”不会产生任何操作。
第二个Demo
当一个线程运行时,另一个线程可以调用对应线程的interrupt()方法来中断线程,
该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。
这里需要注意:如果只是单纯的调用线程对象的interrupt()方法,线程实际并没有被中断,会继续往下执行。
public class SleepInterrupt implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "in run() - 即将睡20秒");
Thread.sleep(20000);
System.out.println("in run() - 开始工作");
} catch (Exception e) {
/**
* 处理完中断异常后,返回run()方法入口;<br>
* 如果没有return,线程实际不会被中断,它会继续往下打印。
*/
System.out.println("in run() - 睡觉时被中断");
e.printStackTrace();
return;
}
System.out.println("in run() - 正常离开");
}
}
public class Run {
public static void main(String[] args) {
SleepInterrupt s = new SleepInterrupt();
Thread t = new Thread(s);
t.setName("Sleep ");
t.start();
// 主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("in main() - 中断其他线程");
// 中断线程
t.interrupt();
System.out.println("in main() - 离开");
}
}
输出结果:
Sleep in run() - 即将睡20秒
in main() - 中断其他线程
in main() - 离开
in run() - 睡觉时被中断
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.demo.concurrent.interrupt.interrupt_2.SleepInterrupt.run(SleepInterrupt.java:11)
at java.lang.Thread.run(Thread.java:745)
捋一下代码:
a. main主线程启动,创建sleep线程对象,并且启动sleep线程。
b. main主线程休眠两秒,让sleep线程有足够时间运行,sleep线程输出"Sleep in run() - 即将睡20秒",然后开始休眠20秒。
c. 两秒之后,main主线程,将sleep线程中断,从而抛出InterruptedException异常,然后输出"in run() - 睡觉时被中断",然后return,
不会执行catch后面的内容
不会执行catch之后的内容,是因为咱们在catch里面,return了
下面咱们把return注释掉试试看
public class SleepInterrupt implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "in run() - 即将睡20秒");
Thread.sleep(20000);
System.out.println("in run() - 开始工作");
} catch (Exception e) {
/**
* 处理完中断异常后,返回run()方法入口;<br>
* 如果没有return,线程实际不会被中断,它会继续往下打印。
*/
System.out.println("in run() - 睡觉时被中断");
e.printStackTrace();
// return;
}
System.out.println("in run() - 正常离开");
}
}
输出结果:
Sleep in run() - 即将睡20秒
in main() - 中断其他线程
in main() - 离开
in run() - 睡觉时被中断
java.lang.InterruptedException: sleep interrupted
in run() - 正常离开
at java.lang.Thread.sleep(Native Method)
at com.demo.concurrent.interrupt.interrupt_2.SleepInterrupt.run(SleepInterrupt.java:11)
at java.lang.Thread.run(Thread.java:745)
从这次的输出结果看,当sleep线程中断,抛出异常之后,sleep似乎又继续往下运行了,输出了"in run() - 正常离开"
所以,使用中断时,在catch中要加入return,结束这个线程的操作。
8. 安全的终止线程
之前的Demo中提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式;
这种交互方式最适合用来取消或停止任务。
除了中断之外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。
public class ThreadA implements Runnable {
private long i;
private volatile boolean on = true;
public void cancel() {
on = false;
}
@Override
public void run() {
// isInterrupted():线程是否被中断(true:已经中断,false:未中断)
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
}
public class ShutDown {
public static void main(String[] args) throws Exception {
// 睡眠1秒,main主线程对ThreadA进行中断,使ThreadA能够感知中断而结束。
Thread t = new Thread(new ThreadA(), "ThreadA");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
// 睡眠1秒,main主线程对TreadA2 进行取消,使ThreadA2能够感知到on为false而结束。
ThreadA a2 = new ThreadA();
Thread t2 = new Thread(a2, "ThreadA2");
t2.start();
TimeUnit.SECONDS.sleep(1);
a2.cancel();
}
}
输出结果:
Count i = 418064275
Count i = 425855781
main主线程通过中断操作和cancel()方法均可使CountThread得以终止。
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断的将线程停止,因此这种终止线程的做法更加安全和优雅。
参考资料: