Java多线程学习(一)- Java线程的生命周期与创建方式

文章目录

  • 前言
  • 一、Java线程的生命周期
  • 二、Java线程的创建及启动
    • 创建方式一:继承Thread类
    • 创建方式二:实现Runnable接口
    • 创建方式三:通过Callable和Future创建线程
      • Callable和Future出现的背景
      • Callable和Future简介
        • 源码分析
      • 通过Callable和Future创建一个线程
  • 三、创建线程的三种方式的对比
    • 采用实现Runnable、Callable接口的方式创见多线程时
    • 使用继承Thread类的方式创建多线程时

前言

我们常说的高并发中的并发并不一定要依赖多线程,如PHP中很常见的并发都是进程级别的多进程并发。但是在Java里面谈论并发,大多数都与线程脱不开关系。那说到线程,就说说Java中线程的基础知识

一、Java线程的生命周期

在书中,将Java线程的生命周期分为了5种状态。我这里将它细分为7种状态,方便理解。

Java线程声明周期状态转换图
Java多线程学习(一)- Java线程的生命周期与创建方式_第1张图片

  • 新建(New)状态: 创建后尚未启动(未调用start()方法)的线程处于这种状态。
  • 就绪(Ready)状态: 当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。
  • 运行(Running)状态: 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
    注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。
  • 无限期等待(Waiting)状态: 处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态。
    • 没有设置Timeout参数的Object.wait()方法
    • 没有设置Timeout参数的Object.wait()方法
  • 限期等待(Timed
    Waiting)状态:
    处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
    • 设置了Timeout参数的Object.wait(long millis)方法
    • 设置了Timeout参数的Object.wait(long millis)方法
  • 阻塞(Blocked)状态: 线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 死亡(Dead)状态: 线程执行完了或者因异常退出了run()方法,该线程生命周期结束。

二、Java线程的创建及启动

Java中线程的创建有三种方式

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,并重写run()方法。

  • 通过Callable和Future创建线程

创建方式一:继承Thread类

步骤

  1. 创建自定义类继承于Thread类,并重写Thread类的run()方法。该run()方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  2. 创建自定义类(Thread子类)的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。

示例

/**
 * Java中创建线程方式一:继承Thread类
 */
public class ThreadTest extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

打印结果

0
1
2
3
4
5
6
7
8
9

创建方式二:实现Runnable接口

步骤

  1. 创建自定义类实现于Runnable接口,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建自定义类(Runnable实现类)的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动该线程。

示例

/**
 * Java中创建线程方式二:实现Runnable接口
 */
public class RunnableTest implements Runnable{

    @Override
    public void run() {
        for (int i = 100; i < 110; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        Thread thread = new Thread(runnableTest);
        thread.start();
    }
}

打印结果

100
101
102
103
104
105
106
107
108
109

创建方式三:通过Callable和Future创建线程

Callable和Future出现的背景

一般创建线程时,使用上面两种方式居多。但是这两种方式都有一个缺陷:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Callable和Future简介

Callable接口可以理解成一段可以调用并返回结果的代码(call方法);
Future接口表示异步任务,是还没有完成的任务给出的未来结果。
所以说Callable用于产生结果,Future用于获取结果。这点可以在源码里面分析得知。

源码分析

先看Runnable源码
Runnable位于java.lang包下,它是一个接口,在它里面声明了一个方法叫做 run():

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

再看Callable源码
Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

Future源码
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。为什么这么说呢?看了它的源码就知道了。
Future类位于java.util.concurrent包下,它也是一个接口

package java.util.concurrent;

public interface Future {

	/**
     * 取消任务
     */
    boolean cancel(boolean mayInterruptIfRunning);

	/**
     * 任务是否被取消成功
     */
    boolean isCancelled();

	/**
     * 任务是否已经完成
     */
    boolean isDone();
	
	/**
     * 获取执行结果
     */
    V get() throws InterruptedException, ExecutionException;

	/**
     * 获取执行结果
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

所以说Future一共给我们提供了三种功能:

  1. 能够取消任务。
  2. 判断任务是否完成。
  3. 能够获取任务执行结果。

但是因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。
FutureTask实现于RunnableFuture接口,这个接口的定义如下:

public interface RunnableFuture extends Runnable, Future {
    void run();
}

可以看到这个接口实现了Runnable和Future接口,接口中的具体实现由FutureTask来实现。这个类的两个构造方法如下 :

	public FutureTask(Callable callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

	public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

如上提供了两个构造函数,一个以Callable为参数,另外一个以Runnable为参数。这些类之间的关联允许你基于FutureTask的Runnable特性(因为它实现了Runnable接口),把任务写成Callable,然后封装进一个由执行者调度并在必要时可以取消的FutureTask。

FutureTask可以由执行者调度,它对外提供的方法基本上就是Future和Runnable接口的组合:get()、cancel、isDone()、isCancelled()和run(),而run()方法通常都是由执行者调用,我们基本上不需要直接调用它。

通过Callable和Future创建一个线程

步骤

  1. 创建自定义类实现于Callable接口,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  2. 创建自定义类(Callable实现类)的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

示例

/**
 * Java中创建线程方式三:Callable和FutureTask结合使用
 */
public class CallableTest implements Callable{

    @Override
    public Object call() throws Exception {
        int i = 1000;
        for ( ; i < 1010; i++) {
            System.out.println(i);
        }
        return 1111;
    }

    public static void main(String[] args) {
        CallableTest callableTest = new CallableTest();
        FutureTask futureTask = new FutureTask(callableTest);
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            System.out.println("Result:"+futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }
}

打印结果

1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
Result:1111

三、创建线程的三种方式的对比

采用实现Runnable、Callable接口的方式创见多线程时

优势是:

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

  • 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时

优势是:

  • 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

  • 线程类已经继承了Thread类,所以不能再继承其他父类。

技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


你可能感兴趣的:(Java多线程)