线程基础

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

写在前面

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352