Java线程的四种创建方式及五种状态

一、多线程的作用

1️⃣发挥多核 CPU 的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至 16 核的也不少见。如果是单线程的程序,那么在双核 CPU 上就浪费了50%,在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的“多线程”是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程“同时”运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让多段逻辑同时工作。多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的。

2️⃣防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换(多线程的上下文切换是指 CPU 控制权由一个正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程),而降低程序整体的效率。但是单核 CPU 还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据,对方迟迟未返回又没有设置超时时间,那么整个程序在数据返回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

3️⃣便于建模
这是另外一个不明显的优点。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务分解成几个小任务:任务 B、任务 C 和任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

二、Java创建线程的四种方式

Java 使用 Thread 类代表线程,所有的线程对象必须是 Thread 类或其子类的实例。Java 可以用四种方式来创建线程,如下:
①继承 Thread 类创建线程没有返回值
②实现 Runnable 接口创建线程没有返回值
③实现 Callable 接口,通过 FutureTask 包装器来创建 Thread 线程有返回值
④线程池:使用 ExecutorService、Callable、Future 实现有返回结果的线程有返回值

1️⃣------------------------继承Thread类创建线程---------------------

  1. 定义 Thread 类的子类,并重写该类的 run(),该方法的方法体就是线程需要完成的任务,run() 也称为线程的执行体。
  2. 创建 Thread 子类的实例,也就是创建了线程对象。
  3. 启动线程,即调用线程的 start()。
    代码实例:
public class MyThread extends Thread{//继承Thread类
  public void run(){ 
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

2️⃣------------------------实现Runnable接口创建线程---------------------

  1. 定义 Runnable 接口的实现类,同样要重写 run()。这个 run() 和 Thread 中的 run() 一样是线程的执行体。
  2. 创建 Runnable 实现类的实例,并用这个实例作为 Thread 的 target 来创建 Thread 对象,这个 Thread 对象才是真正的线程对象。
  3. 依然是通过调用线程对象的 start() 来启动线程。
public class MyThread implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread myThread=new MyThread();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者new Thread(new MyThread()).start();
  }
}

3️⃣------------------------使用Callable和Future创建线程---------------------

不同于 Runnable 接口,Callable 接口提供了一个 call() 为线程的执行体,call() 比 run() 功能要强大:

  1. call() 可以有返回值;
  2. call() 可以声明抛出异常。

Java5 提供了 Future 接口来代表 Callable 接口里 call() 的返回值,并且为 Future 接口提供了一个实现类 FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务:

  1. boolean cancel(boolean mayInterruptIfRunning):试图取消该 Future 里面关联的 Callable 任务。
  2. get():返回 Callable 里 call() 的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。
  3. get(long timeout,TimeUnit unit):返回 Callable 里 call() 的返回值,最多阻塞 timeout 时间,经过指定时间没有返回抛出 TimeoutException。
  4. boolean isDone():若 Callable 任务完成,返回 true。
  5. boolean isCancelled():如果在 Callable 任务正常完成前被取消,返回 true。

创建并启动有返回值的线程的步骤如下:

  1. 创建 Callable 接口的实现类,并实现 call(),然后创建该实现类的实例(从 Java8 开始可以直接使用 Lambda 表达式创建 Callable 对象)。
  2. 使用 FutureTask 类来包装 Callable 对象及 call() 的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程(因为 FutureTask 实现了 Runnable 接口)。
  4. 调用 FutureTask 对象的 get() 来获得子线程执行结束后的返回值。
public class Main {
  public static void main(String[] args){
   //使用Lambda表达式创建Callable对象
    //使用FutureTask类来包装Callable对象
   FutureTask<Integer> task = new FutureTask<Integer>(
    (Callable<Integer>)()->{
      return 5;
    }
    );
   new Thread(task,"有返回值的线程").start();
   //实质上还是以Callable对象来创建并启动线程
    try{
    System.out.println("子线程的返回值:"+task.get());
    //get()方法会阻塞,直到子线程执行结束才返回
    }catch(Exception e){
    ex.printStackTrace();
   }
  }
}

4️⃣----------使用ExecutorService、Callable、Future实现有返回结果的线程--------

ExecutorService、Callable、Future 三个接口实际上都是属于 Executor 框架。返回结果的线程是在 JDK1.5 中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。有返回值的任务必须实现 Callable 接口。类似的,无返回值的任务必须实现 Runnable 接口。
执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object
注意:get()是阻塞的,线程无返回结果,get()会一直等待。
再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。如下是一个完整的有返回结果的多线程例子:

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

//有返回值的线程
@SuppressWarnings("unchecked")
public class Test {
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        System.out.println("----程序开始运行----");
        long start = System.currentTimeMillis();
        int taskSize = 5;
        // 创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < taskSize; i++) {
            Callable c = new MyCallable(i + " ");
            // 执行任务并获取Future对象
            Future f = pool.submit(c);
            list.add(f);
        }
        // 关闭线程池
        pool.shutdown();
        // 获取所有并发任务的运行结果
        for (Future f : list) {
            // 从Future对象上获取任务的返回值,并输出到控制台
            System.out.println(">>>" + f.get().toString());
        }
        long end = System.currentTimeMillis();
        System.out.println("----程序结束运行----,程序运行时间【" + (end - start) + "毫秒】");
    }
}
class MyCallable implements Callable<Object> {
    private String taskNum;
    MyCallable(String taskNum) {
        this.taskNum = taskNum;
    }
    @Override
    public Object call() throws Exception {
        System.out.println(">>>" + taskNum + "任务启动");
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println(">>>" + taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间【" + (end - start) + "毫秒】";
    }
}

三、对比

Runnable 接口中的 run() 的返回值是 void,它做的事情只是纯粹地去执行 run() 中的代码而已;Callable 接口中的 call() 是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了,某条线程执行了多久,某条线程执行时期望的数据是否已经赋值完毕。无法得知,能做的只是等待这条多线程的任务执行完毕而已。而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。

实现 Runnable 接口和实现 Callable 接口的方式基本相同。可以把这两种方式归为一种,这种方式与继承 Thread 类的方法之间的差别如下:

  1. 线程只是实现 Runnable 接口或实现 Callable 接口,还可以继承其他类。
  2. 这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。
  3. 编程稍微复杂。如果需要访问当前线程,必须调用 Thread.currentThread()。
  4. 继承 Thread 类的线程类不能再继承其他父类( Java 单继承)。

注:一般推荐采用实现接口的方式来创建多线程。

四、线程的生命周期

Java 使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  1. 【新建】当一个 Thread 类或其子类的对象被声明并创建时,并没有调用该对象的 start(),新生的线程对象处于新建状态。
  2. 【就绪】当调用了线程对象的 start() 之后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件。因为线程调度程序还没有把该线程设置为当前线程,所以此时该线程处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  3. 【运行】当就绪的线程被调度并获得处理器资源时,便进入运行状态,开始执行 run() 当中的代码。
  4. 【阻塞】线程正在运行的时候,在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep 和 wait 等方法都可以导致线程阻塞。

注意:

  • Thread.sleep() 让线程从 running--->阻塞态。时间结束/interrupt--->runnable
  • Object.wait() 让线程从 running--->等待队列。notify--->锁池--->runnable
  1. 【死亡】如果一个线程的 run() 执行结束或者调用 stop() 后,该线程就会死亡。对于已经死亡的线程,无法再使用 start() 令其进入就绪。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,629评论 0 12
  • 一.线程与进程相关 1.进程   定义:进程是具有独立功能的程序关于某个数据集合上的一次运行活动,进程是操作系统分...
    Geeks_Liu阅读 1,644评论 2 4
  • 课堂笔记 作者:郭浩祥 归档:课堂笔记 时间:2019.4.3 作业: 1、 全网搜集:TCP/IP相关面试题。归...
    大路上最强的男人阅读 168评论 0 0
  • 在这充满未知的时空里茫然地走,是想把不甘的痴心随意地放牧到不死不休。随处是狂舞着的一双双干瘪、腥臭而劲道的手...
    天涯牧歌阅读 290评论 2 5
  • 听着耳边循环播放的喜欢的古风歌曲,耳机中夹杂着风吹过的声音。张开双臂,去拥抱太阳和清风,闭上双眼,去用心感受...
    只喜欢柚子的老丸子阅读 497评论 2 2