多线程基础(二)| 线程的优先级、CountDownLatch 和CyclicBarrier

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();
    }
}

运行结果为:
在这里插入图片描述

优先级的特点:

  1. 线程优先级的继承特性:也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;
  2. 线程优先级的规则性:即线程会优先级的大小顺序执行,但是不一定是优先级较大的先执行完,因为线程的优先级还有下面第三个特性:
  3. 线程优先级的随机特性优先级越高,大部分情况下在多个线程的情况下优先级高的先完成所有的任务。但是优先级并不是衡量那个线程运行结果顺序的标准,因为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(阻塞)、waitingtimed_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的线程安全问题将会在下一篇博客中讲解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值