Android进阶之路Android开发经验谈Android技术知识

线程基础

2019-08-07  本文已影响10人  Chase_stars

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

写在前面

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

进程和线程

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

任务管理器.png

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

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

为何使用多线程

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

线程的状态

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

线程的状态.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()方法不会立即执行多线程的代码,而是让该线程变为可运行状态,什么时候运行多线程的代码是由操作系统决定的。

实现步骤:

    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方法

实现步骤:

    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接口更强大的功能,主要体现在以下三个方面:

实现步骤:

    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异常被捕获后一定要做处理。

上一篇 下一篇

猜你喜欢

热点阅读