多线程基础(二)| 线程的优先级、线程的生命周期及相关知识
1.线程优先级
线程优先级的设置和获取分别可以用setPriority()
方法和getPriority()
方法。
和优先级队列一样,每个线程有一个队行的优先级的数值来判断线程优先级的高低,取值范围为1~10,数字越大优先级越高,默认值为5。
理论上讲优先级高的先被执行。实际上,优先级高只是被cpu先执行概率大,并不是说优先级高的一定就会先被执行。因此我们在项目开发中不能使用优先级来完成一些特定的任务。
优先级的继承性
若B线程是在A线程中创建出来的,那么称B线程是A线程的子线程,这两个线程具有相同的优先级。
class ThreadA extends Thread {
@Override
public void run() {
this.setPriority(1);
System.out.println("A线程的优先级为:" + this.getPriority());
ThreadB threadB = new ThreadB();
threadB.start();
}
}
class ThreadB extends Thread {
@Override
public void run() {
System.out.println("B线程的优先级为:" + this.getPriority());
}
}
public class TestApp {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
threadA.start();
}
}
运行结果为:
优先级的特点:
- 线程优先级的继承特性:也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;
- 线程优先级的规则性:即线程会优先级的大小顺序执行,但是不一定是优先级较大的先执行完,因为线程的优先级还有下面第三个特性:
- 线程优先级的随机特性优先级越高,大部分情况下在多个线程的情况下优先级高的先完成所有的任务。但是优先级并不是衡量那个线程运行结果顺序的标准,因为cpu只是尽量将执行资源交给优先级高的线程。优先级高的线程不一定每一次都先执行完run方法中的任务,也就是说线程优先级与打印顺序无关,他们的关系具有不确定性和随机性。
2.守护线程
守护线程是一个特殊的线程。它是一个在后台执行的线程,当所有非守护线程结束之后守护线程会自动结束。
如下面实例的程序,把子线程设置为守护线程,子线程的执行逻辑为一个死循环,一直打印子线程的名字。等到七秒之后,非守护线程主线程执行完成,子线程也自动结束,子线程的死循环结束。
public class DaemonTest {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName()); // 子线程一直打印
}
}
});
t.setDaemon(true); // 把子线程设置成守护线程 一定要在线程启动之前
t.start();
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()); // 主线程名字
}
}
守护线程的使用场景: 垃圾回收、服务器线程等。 垃圾是用户创建出来的线程产生的,当用户线程如果结束,就没有必要再去回收了。
3.线程的生命周期
线程的生命周期指的就是线程从生到死的过程。
我们知道,要开启一个线程,不管怎么样,都是要创建一个Thread对象的,那么线程的状态都是定义在Thread类下的,从源码中可以找到总共有New(新建)、Runnable(运行)、blocked(阻塞)、waiting、timed_waiting(等待)、terminated(终止)这几个状态。
线程的New状态:
- 当我们用关键字new创建一个thread对象时,此时它并不是出于执行状态,因为没有调用start方法启动该线程,那么线程的状态为new状态。准确地说,它只是Thread对象的状态,因为没有start之前,该线程根部就不存在,与你用关键字new创建一个普通的java对象没有什么区别new状态通过
start()
方法进入runnable状态。
线程的Runnable状态:
- Runnable状态:分成了两种状态,可执行(就绪态)和运行态。运行态顾名思义。就绪态指的是已经调用了start方法,但是线程还没有被cpu调度执行,只是具有了运行的资格。可执行(就绪态)的线程只能意外终止或者进入运行状态。
线程的blocked状态:
- 线程阻塞状态。当程序进行某个阻塞的io操作,比如因网络数据的读写而进入的blocked状态,或获取某个锁资源,等待时,加入到该锁的阻塞队列中进入blocked状态。
线程的waiting状态:
- 一个线程在等待执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后不能自动唤醒,必须等待另一个线程发出特定的指令才能够被唤醒。
线程的timed_waiting状态:
- 和上面的类似,只是这个状态的线程不会一直等下去,会有一个超时时间。
线程的terminated状态
- terminated是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入terminated状态,意味着该线程的整个生命周期结束了。
线程运行正常结束,结束生命周期,或线程运行出错意外结束。JVM crash会导致所有的线程都结束。
NEW、RUNNABLE、TERMINATED这三个状态时一个线程必定会经历的,其余几个状态只有当线程遇到某种状态或执行了某个方法才会经历。
4.线程join
调用join()
方法可以让其他线程处于waiting状态。
如下面实例,我们分别在子线程调用join()
方法前后获取一下主线程的状态
class JoinThread extends Thread {
Thread thread;
public JoinThread(String str, Thread thread) {
super(str);
this.thread = thread;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(this.getName());
System.out.println("state: " + thread.getState());
}
}
}
public class TestApp {
public static void main(String[] args) { //主线程
JoinThread thread = new JoinThread("子线程",Thread.currentThread());
thread.start(); //启动子线程
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程waiting 那么后面的代码在这段时间将不会被执行 直到调用join方法的这个线程执行完之后才会执行后续代码
System.out.println("state: " + Thread.currentThread().getState()); //主线程被唤醒执行后的状态
for (int i = 0; i < 4; i++) {
System.out.println("主线程");
}
}
}
运行结果如图所示,可以看到,在调用了join()
方法之后,主线程一直处于等待状态,直至子线程执行完毕:
若不调用join方法 ,那么子线程和主线程是同时在执行的,而不是如图先执行了子线程再执行了主线程。
通过实例,来看一下join()
方法的应用场景,假设你负责一个旅游app的航班功能,分别推送三个航空公司的航班时间给用户。
我们可以启动三个线程,分别代表三家公司,主线程负责打印最后的结果,当三个子线程完成查询工作后,主线程才开始自己的功能执行。(为了方便,我们只设置8:00的航班)
我们可以让三个子线程分别调用join()
方法,使主线程处于等待状态。代码如下:
class AirPlaneSearch implements Runnable {
List<String> list;
public AirPlaneSearch(List<String> list) {
this.list = list;
}
@Override
public void run() { //查询各个航空公司的信息
System.out.println("开始从" + Thread.currentThread().getName() + "查询航班信息");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list) { // 加上线程锁 避免同时多个线程执行add操作
list.add(Thread.currentThread().getName() + "8:00");
}
}
}
/**
* 使用join方法
*/
public class AirLine {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
AirPlaneSearch runnable = new AirPlaneSearch(list);
Thread t1 = new Thread(runnable, "南方航空");
Thread t2 = new Thread(runnable, "东方航空");
Thread t3 = new Thread(runnable, "海南航空");
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Iterator<String> iterator = list.iterator();
System.out.println("--------------------------------------------");
System.out.println("所查询的航班信息如下: ");
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
运行结果如下:
在子线程的功能完成之后才输出list中的航班信息
5.CountDownLatch 和CyclicBarrier
用CountDownLatch可以避免写太多的join代码,先new出来一个CountDownLatch
对象,将需要进行join操作的线程个数当作参数传递给构造函数,在子线程结束的时候对CountDownLatch进行减一操作,直至减为0后主线程开始执行它的功能。
还是以航班信息为例,代码如下:
class AirPlaneSearch2 implements Runnable {
List<String> list;
CountDownLatch latch ;
public AirPlaneSearch2(List<String> list, CountDownLatch latch) {
this.list = list;
this.latch = latch;
}
@Override
public void run() { //查询各个航空公司的信息
System.out.println("开始从" + Thread.currentThread().getName() + "查询航班信息");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list) {
list.add(Thread.currentThread().getName() + "8:00");
}
latch.countDown(); // count执行了一次减一
}
}
/**
* 使用CountDownLatch
*/
public class AirLine2 {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
CountDownLatch latch = new CountDownLatch(3); //有几个子线程就设置为几
AirPlaneSearch2 runnable = new AirPlaneSearch2(list,latch);
Thread t1 = new Thread(runnable, "南方航空");
Thread t2 = new Thread(runnable, "东方航空");
Thread t3 = new Thread(runnable, "海南航空");
t1.start();
t2.start();
t3.start();
try {
latch.await(); //等待latch中的count被减为 0 什么时候减为 0 后面的代码才开始执行
} catch (InterruptedException e) {
e.printStackTrace();
}
Iterator<String> iterator = list.iterator();
System.out.println("--------------------------------------------");
System.out.println("所查询的航班信息如下: ");
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
运行结果如下:
CyclicBarrier的参数和CountDownLatch差不多,按航班信息的例子来说,参数依然是3,不过CyclicBarrier不用使用方法进行减一操作,它会自动减一,直至0才开始执行中另一个参数Runnable()
中的代码。
class AirPlaneSearch3 implements Runnable {
List<String> list;
CyclicBarrier barrier;
public AirPlaneSearch3(List<String> list, CyclicBarrier barrier) {
this.list = list;
this.barrier = barrier;
}
@Override
public void run() { //查询各个航空公司的信息
System.out.println("开始从" + Thread.currentThread().getName() + "查询航班信息");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list) {
list.add(Thread.currentThread().getName() + "8:00");
}
//遇到awit自动减一
try {
//所有的线程相互等待直到把barrier中的值减为0
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
/**
* 使用CyclicBarrier
*/
public class AirLine3 {
public static void main(String[] args) {
final List<String> list = new LinkedList<>();
// 什么时候barrier 减为0 Runnable中的方法被执行
CyclicBarrier barrier = new CyclicBarrier(3, new Runnable(){
@Override
public void run() {
Iterator<String> iterator = list.iterator();
System.out.println("--------------------------------------------");
System.out.println("所查询的航班信息如下: ");
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
});
AirPlaneSearch3 runnable = new AirPlaneSearch3(list,barrier);
Thread t1 = new Thread(runnable, "南方航空");
Thread t2 = new Thread(runnable, "东方航空");
Thread t3 = new Thread(runnable, "海南航空");
t1.start();
t2.start();
t3.start();
}
}
运行结果如下:
由代码可以看出来:由于使用起来并不灵活,一般CyclicBarrier在实际开发中并不常用。
6.线程中断
上面说到,调用join()
方法的线程可以试别的线程进入等待状态,在开发中可能会遇到让线程的等待状态被打断的情况,调用interrupt()
方法,可以中断一个线程的等待状态。
以下面实例来说,子线程调用join()
方法后主线程进入等待状态,直到子线程打印操作结束后主线程才开始执行,而当第3次(由于cpu的调度,不一定正好三次)打印操作执行完后调用interrupt()
方法,可以直接抛出异常,使得主线程的等待状态被打断。
class JoinThread extends Thread {
Thread thread;
private static int i = 0;
public JoinThread(String str, Thread thread) {
super(str);
this.thread = thread;
}
@Override
public void run() {
while(true) {
i++;
System.out.println(this.getName());
if (i == 3) {
thread.interrupt();
}
}
}
}
public class TestApp {
public static void main(String[] args) {
JoinThread thread = new JoinThread("子线程",Thread.currentThread());
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 4; i++) {
System.out.println("主线程");
}
}
}
运行结果如下:
线程中断的方法:
Object的wait/wait (1ong) /wait (long, int)方法
Thread的sleep (int) /sleep (long, int)
Thread的join/join(long) /join(long, int)方法
。。。。。。
为什么调用了interrupt方法就会让线程的waiting状态被打断?
答:调用了interrupt方法,会将线程中断位(interrupt
flag)由false变成true,而在join方法会判断线程中断位的状态,如果为true就会抛出异常,从而结束线程的等待状态。如果异常被捕获到,线程中断位的值就会再变成false(被擦除)。
获取线程中断位的方法可以通过实例方法this.isInterrupt()或静态方法Thread.interrupt(),两者的区别为后者再中断位被置为true后调用,线程中断位的值会被擦除,而前者不会主动擦除。
7.如何关闭一个线程
一个线程的结束再正常情况下即为一个线程的生命周期正常结束掉,通俗来说就是线程的run()
方法从第一行代码执行到了最后一行。
还有一种非正常结束,当这个线程还在工作当中,但不希望它继续工作下去了,就要采取一些方式让他结束掉。
利用对线程中断位的值的控制可以做到对线程进行非正常的关闭
class SonThread implements Runnable {
@Override
public void run() {
while(true) {
if(Thread.currentThread().isInterrupted()){
break;
}
System.out.println("子线程执行中");
}
}
}
public class TestApp {
public static void main(String[] args) {
SonThread runnable = new SonThread();
Thread t = new Thread(runnable);
t.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
当三秒之后调用interrupt,此时线程中断位置为false,子线程break出死循环,线程结束。
缺点:如果程序在其他地方调用了类似方法(如sleep、join等),那么中断位的值是会被擦除的,导致程序出现错误的几率变高。
捕获异常的方式同理:
class SonThread implements Runnable {
@Override
public void run() {
while(true) {
try {
TimeUnit.NANOSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
break; // 捕获到异常之后要跳出循环
}
System.out.println("子线程执行中");
}
}
}
public class TestApp {
public static void main(String[] args) {
SonThread runnable = new SonThread();
Thread t = new Thread(runnable);
t.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
运行结果如下,三秒之后线程中断位值改变,抛出异常,结束线程:
抛出异常的处理方式的缺点:执行逻辑发生了改变 并且执行效率受到影响。
上面两种方式都有各自的缺点,在实际的项目开发当中并实用,有一种更好的处理方式,可以避免这两种情况的缺点。
我们给子线程提供一个静态变量flag,和一个关闭线程的静态方法。在run方法中定义:当flag的值变成true
的时候break,在主线程处三秒后调用该静态方法使flag的值发生改变。
class SonThread implements Runnable {
private static volatile boolean flag = false;
public static void CloseThread() {
flag = true;
}
@Override
public void run() {
while(true) {
if (flag) {
break;
}
System.out.println("子线程执行中");
}
}
}
public class TestApp {
public static void main(String[] args) {
SonThread runnable = new SonThread();
Thread t = new Thread(runnable);
t.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
SonThread.CloseThread();
}
}
这样既不会抛出异常,也不会出现值被擦除的现象。
关于volatile的线程安全问题将会在下一篇博客中讲解