线程基础

作者: Chase_stars | 来源:发表于2019-08-07 21:32 被阅读10次

谁不会休息,谁就不会工作。 — 列宁

写在前面

Android沿用了Java的线程模型,一个Android应用创建的时候就会开启一个线程,我们叫它主线程或者UI线程,如果想要访问网络或数据库等耗时操作时,就会开启一个子线程去处理。从Android3.0开始,系统规定网络访问必须在子线程中进行,否则会抛出异常。就是为了避免因耗时操作阻塞主线程从而发生ANR,也证明了多线程在Android应用开发中占据着十分重要的位置。

进程和线程

在了解线程之前,我们先来了解一下进程,什么是进程?进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统资源分配和调度的基本单元,进程可以被看做是程序的实体,进程也是线程的容器。这么说过于抽象,下面打开我们电脑的任务管理器:

任务管理器.png

从图片中可以看到“应用”下面包含三个前台进程,“后台进程”有五十七个,每一个进程就对应一个应用。

上面图片中"WeChat(32)位"对应微信这个应用的进程,这个进程里又运行了许多子任务,有的处理缓存,有的进行下载,有的接收消息,这些子任务就是线程,是操作系统调度的最小单元,也叫做轻量级的进程。一个进程可以创建多个线程,可以创建的线程数量取决于操作系统,每个线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存数据。

为何使用多线程

从操作系统级别上来看主要有以下四个方面:

  • 使用多线程能够减少程序响应的时间。如果某个操作耗时,或是陷入长时间的等待,就不能响应鼠标或键盘等操作,使用多线程能够将耗时操作放到一个单独的线程中执行,从而使程序具备了更好的交互性。
  • 与进程相比,线程的创建和切换开销更小,在访问共享数据方面效率非常高。
  • 多CPU或多核计算机本身就具备执行多线程的能力。如果执行单个线程,就无法重复利用计算机的资源,从而导致巨大的资源被浪费,在多CPU的计算机中使用多线程能够有效提升CPU的利用率。
  • 使用多线程能够简化程序结构,使程序便于理解和维护。

线程的状态

Java线程在运行的生命周期中会有六种不同的状态:

  • New:新创建状态。线程创建,但还没有调用start函数,线程运行之前还有一些基础工作要做。
  • Runnable:可运行状态。一旦调用start函数,线程就处于可运行状态,可运行状态的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
  • Blocked:阻塞状态。表示线程被锁阻塞,暂不活动。
  • Waiting:等待状态。表示线程暂不活动,并且不运行任何的代码,直到线程调度器重新激活它。
  • Timed Waiting:超时等待状态。与等待状态不同,超时等待状态的线程会在指定的时间自行返回。
  • Terminated:终止状态。表示线程执行完毕,终止线程有两种情况,第一种是线程的run函数执行完毕正常退出,第二种情况是因为一个没有捕获的异常而终止了run函数,导致线程进入终止状态。
线程的状态.png

从上图可以看到新创建状态(NEW)调用start()函数进入到可运行状态(RUNNABLE)。调用wait()等函数使可运行状态(RUNNABLE)的线程进入到等待状态(WAITING),调用notify()等函数使等待状态(WAITING)的线程回到可运行状态(RANNABLE)。超时等待状态(TIMED WAITING)就是在等待状态(WAITING)的基础上加入了时间的限制,达到指定的时间线程会从超时等待状态(TIMED WAITING)自行回到可运行状态(RUNNABLE)。调用同步方法时,线程获取不到锁就会进入阻塞状态(BLOCKED),直到线程再次获取到锁才会从阻塞状态(BLOCKED)回到可运行状态(RUNNABLE)。直到run()函数执行完毕或发生异常意外终止,线程才会变为终止状态(TERMINATED)。

创建线程

多线程的实现方法一般有三种,其中前两种为最常用的方法。

1.继承Thread类,重写run()方法

Thread类本质上是实现了Runnable接口的一个实现类,需要注意的是调用了start()方法不会立即执行多线程的代码,而是让该线程变为可运行状态,什么时候运行多线程的代码是由操作系统决定的。

实现步骤:

  • 定义一个Thread的子类,并重写run()方法,该run()方法的方法体就代表了该线程要完成的任务,因此,run()方法被称为执行体。
  • 创建Thread的子类对象,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
    public static void main(String[] args) {
        Thread thread = new ChildThread();
        thread.start();
    }

    private static class ChildThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.print("执行体");
        }
    }
2.实现Runnable接口,并实现该接口的run方法

实现步骤:

  • 定义一个类并实现Runnable接口,实现run()方法。
  • 定义Thread的子类对象,并使用实现Runnable接口的对象作为参数实例化该Thread的子类对象。
  • 调用Thread的子类对象的start()方法来开启线程。
    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        Thread thread = new Thread(runnable);
        thread.start();
    }
    
    private static class RunnableImpl implements Runnable {

        @Override
        public void run() {
            System.out.print("执行体");
        }
    } 
3.实现Callable接口,实现call()方法

Callable接口实际上是Executor框架中的功能类,Callable接口和Runnable接口功能类似,但提供了比Runnable接口更强大的功能,主要体现在以下三个方面:

  • Callable接口可以在任务结束后提供一个返回值,Runnable接口无法提供这个功能。
  • Callable接口中的call()方法能抛出异常,Runnable接口中的run()方法不能抛出异常。
  • 运行Callable接口可以得到一个Futrue对象,Futrue对象表示异步计算的结果,它提供了检查异步计算是否完成的方法。由于线程属于异步计算模型,因此无法从其他线程得到函数的返回值,这种情况就需要Future对象监视目标线程执行call()方法的情况。但调用Futrue的get()方法以获取结果时,当前线程就会阻塞,直到call()方法返回结果。

实现步骤:

  • 定义一个类实现Callable接口,并实现call()方法将计算结果作为返回值。
  • 创建线程池ExecutorService,使用实现Callable接口的对象作为参数传给ExecutorService的submit(callable)来开启线程,并接收Futrue对象。
  • 调用Futrue对象的get()方法获取结果。
    public static void main(String[] args) {
        CallableImpl callable = new CallableImpl();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        
        Future future = executorService.submit(callable);

        try {
            String result = (String) future.get();
            System.out.print(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class CallableImpl implements Callable {

        @Override
        public String call() throws Exception {
            return "计算结果";
        }
    }

中断线程

当线程的run方法执行完毕,或者在方法中出现了没有捕获的异常时,线程将终止。

在Java的早期版本中提供了一个stop方法,其他线程调用stop方法就会终止线程,由于该方法会带来一些问题,现在已经被弃用了。

interrupt方法可以用来请求中断线程,一个线程调用interrupt方法就会将线程的中断标识位置为true,线程会不时的检测线程的中断标识位以判断线程是否可以被中断。要想知道线程是否可以被中断,可以调用Thread.currentThread().isInterrupted()方法。

还可以调用interrupted方法对线程的中断标识位进行复位,但是如果线程被阻塞,将无法判断中断状态。如果一个线程处于阻塞状态,线程在检测线程的中断标识位时发现线程的中断标识位为true,就会在方法调用出抛出InterruptedException异常,并且在抛出异常前将线程的中断标识位重置,即设置为false。

需要注意的是被中断的线程并不一定会终止,中断线程只是为了引起线程的注意,被中断的线程决定是否响应中断。如果是非常重要的线程则不理会中断,但大部分情况是线程将中断作为一个终止的请求。另外,不要在底层代码中捕获InterruptedException异常后不做处理。如下:

    private static class ChildThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                sleep(500);
            } catch (InterruptedException e) {
                // do something
            }
        }
    }

推荐两种捕获InterruptedException异常后正确的处理方式:

  • 第一种方式
    public static void main(String[] args) {
        Thread thread = new ChildThread();
        // 开启子线程
        thread.start();
        // 让当前线程阻塞20毫秒
        TimeUnit.MILLISECONDS.sleep(20);
        // 设置线程的中断标识位为true
        thread.interrupt();
    }

    private static class ChildThread extends Thread {
        @Override
        public void run() {
            super.run();
            doing();
        }

        private void doing() {
            // 让子线程阻塞500毫秒,500毫秒之内如果调用了子线程的interrupt方法,
            // 就会抛出InterruptedException异常,在方法内部直接捕获。
            try {
                sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
  • 第二种方式
    public static void main(String[] args) {
        Thread thread = new ChildThread();
        // 开启子线程
        thread.start();
        // 让当前线程阻塞20毫秒
        TimeUnit.MILLISECONDS.sleep(20);
        // 设置线程的中断标识位为true
        thread.interrupt();
    }

    private static class ChildThread extends Thread {
        @Override
        public void run() {
            super.run();
            // 调用者捕获InterruptedException异常
            try {
                doing();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        private void doing() throws InterruptedException{
            // 让子线程阻塞500毫秒,500毫秒之内如果调用了子线程的interrupt方法,
            // 就会抛出InterruptedException异常,将异常向外抛出。
           sleep(500);
        }
    }

由于抛出InterruptedException异常之前会将线程的中断标识位复位,即设置为false,所以在catch语句中再调用一次Thread.currentThread().interrupt()方法将线程的中断标识位设置为true,这样外部再次调用Thread.currentThread().isInterrupted()方法时就知道线程应该被中断了。

安全的中断线程

在前面了解到了如何中断线程,现在就来看看如何安全的中断线程,这里介绍两种安全的中断线程的方式。

  • 第一种方式
    public static void main(String[] args) {
        Thread thread = new ChildThread();
        // 开启子线程
        thread.start();
        // 让当前线程阻塞500毫秒
        TimeUnit.MILLISECONDS.sleep(500);
        // 设置线程的中断标识位为true
        thread.interrupt();
    }

    private static class ChildThread extends Thread {
        @Override
        public void run() {
            super.run();
            // 判断线程的中断标识位是否为中断状态,
            // 如果没有被中断则会一直调用doing,否则会被中断
            while (!Thread.currentThread().isInterrupted()) {
                doing();
            }
        }

        private void doing() {
            System.out.print("执行任务");
        }
    }
  • 第二种方式
    public static void main(String[] args) {
        ChildThread thread = new ChildThread();
        // 开启子线程
        thread.start();
        // 让当前线程阻塞500毫秒
        TimeUnit.MILLISECONDS.sleep(500);
        // 设置线程的中断标识位为true
        thread.cancel();
    }

    private static class ChildThread extends Thread {

        private volatile boolean isCancel;

        @Override
        public void run() {
            super.run();
            // 判断isCancel标识位是否为true,
            // 如果isCancel标识位不为true则会一直调用doing,否则会被中断
            while (!isCancel) {
                doing();
            }
        }

        private void doing() {
            System.out.print("执行任务");
        }

        public void cancel() {
            isCancel = true;
        }
    }

其实以上两种方式的原理相同,都是通过一个标识位来判断当前线程是否应该被中断。

总结

虽然线程的stop方法被弃用了,但是却有中断线程这种替代方案。线程的中断标识位只不过是一个普通的标识位,开发者可以根据线程的中断标识位来决定是否应该中断线程,可以根据实际的项目需求使用线程的中断标识位,如果是非常重要的线程则不理会即可。但是对于一个处于阻塞状态的线程,若线程在检测线程的中断标识位时发现线程的中断标识位为true时,抛出的InterruptedException异常被捕获后一定要做处理。

相关文章

网友评论

    本文标题:线程基础

    本文链接:https://www.haomeiwen.com/subject/epzwdctx.html