线程基础与Executor


线程概述

线程在现代操作系统中的大致分两种(参考来源:https://developer.aliyun.com/article/641914

  1. 内核级线程(KLT,Kernel Level Thread)

    线程管理的所有工作(创建和撤销)由操作系统内核完成;操作系统内核提供一个应用程序设计接口API,供开发者使用KLT;

    线程交给内核管理,内核需要维护线程表,线程表保存了寄存器、状态和其他信息。当然每个线程所属进程的进程表也是维护在内核的,内核创建和销毁线程的代价是比较大的。

  2. 用户级线程(ULT,User Level Thread)

    应用程序可以通过使用用户空间运行线程库被设计成多线程程序。线程的创建,消息传递,调度,保存/恢复上下文都有线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)。

    对照一下,就是将线程的调度放在用户态执行,对于内核来说就像是单线程一样。

Java线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系,当然也有其它映射关系如1:N、N:N,如go语言使用的就是1:N,所以常常go语言被硬吹并发比java高很多就归功于其线程模型。

KLT与ULT(图片来自于网络)

KLT与ULT

Java线程基础

创建线程的方式

  1. 继承Thread类,重写run方法,新建Thread类对象,使用该对象调用Thread类的start方法启动线程

    1
    2
    3
    4
    5
    6
    public class MyWorker extends Thread {
    @Override
    public void run() {
    // todo
    }
    }
    1
    2
    3
    4
    public static void main(String[] args) {
    Thread myWorker = new MyWorker();
    myWorker.start();
    }
  2. 实现Runnable接口,使用Runnable的实例作为构造方法的参数新建Thread类对象,调用Thread对象的start方法创建线程

    1
    2
    3
    4
    5
    6
    public class MyRunner implements Runnable {
    @Override
    public void run() {
    // todo
    }
    }
    1
    2
    3
    4
    5
    public static void main(String[] args) {
    MyRunner myRunner = new MyRunner();
    Thread thread = new Thread(myRunner);
    thread.start();
    }
  3. 实现Callable接口,将Callable实例提交到一个线程池(见下文)ExecutorService中执行任务;准确的说这并不算是创建线程的一个方式,因为ExecutorService接收任务后并不一定创建线程去执行。

    1
    2
    3
    4
    5
    6
    7
    class MyCaller implements Callable<String> {
    @Override
    public String call() throws Exception {
    Thread.sleep(3000);
    return "call end";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) {
    MyCaller myCaller = new MyCaller();
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    try {
    Future<String> future = executorService.submit(myCaller);
    String result = future.get();
    System.out.println(result);
    }finally {
    executorService.shutdown();
    }
    }

Callable接口出现的必然性

Callable接口与Runnable接口:Callablecall方法有返回值的,且允许抛出异常,Runnable接口的run方法无返回值,不允许抛出异常(这里的异常肯定是检查型异常);

1
2
3
4
5
6
7
8
9
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

jdk提供Runnable接口,我们可以在当前线程之外开启子线程去干一些事情(异步),根据Runnable接口的run方法的特点,这个子线程执行了就执行了,我们不清楚这个线程允许情况如何,有时候我们希望子线程执行结束后,我们能拿到些东西,Runnable接口显然并不能满足,于是Callable接口出现了。

显然“任务”这个概念更适合我们对子线程的描述,我们可以这样理解:

Runnable是一个不期望有结果的任务,执行了就执行了,自己做好异常处理;Callable是一个期望有结果的任务,我们可以关心它抛出的异常。

下文《Executor框架》将会讲解任务相关的管理。

线程生命周期

网上众多讲java线程生命周期时想必都会贴出下面这么一张图,然后就人云亦云

准确的说,上述这5个状态是从cpu角度看操作系统的线程各个阶段的状态。前文说到Java中的线程是KLT,所以Java线程在cpu角度看也是有这5个状态,但是不代表Java就是如此定义线程状态的!!!

JavaThread类使用内部枚举State来标识线程的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {

NEW,

RUNNABLE,

BLOCKED,

WAITING,

TIMED_WAITING,

TERMINATED;
}

可以看到总共有6个状态,下面一一举例

  1. NEW:新生态,线程对象被创建时处于该状态

    1
    2
    3
    4
    5
    public static void main(String[] args) {

    Thread t = new Thread();
    System.out.println(t.getState());
    }
  2. RUNNABLE:运行态,线程正在运行其线程栈内代码(指令)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) throws InterruptedException {

    Thread t = new Thread(() -> {

    long start = System.currentTimeMillis();
    for (;;) {

    if (System.currentTimeMillis() - start > 1000 * 3) // 循环3秒以上
    break;
    }
    });
    t.start();

    Thread.sleep(1000L);// sleep 1秒
    System.out.println(t.getState());// 此时线程t未结束,正在执行其循环代码
    }
  3. BLOCKED:阻塞状态,线程想获取不到synchronized同步锁时,进入阻塞状态,如下线程t2获取target对象的锁时,因为线程t1还未释放,所以t2进入阻塞状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public static void main(String[] args) throws InterruptedException {

    Object target = new Object();

    Thread t1 = new Thread(() -> {

    synchronized (target) {// 获取target的锁
    long start = System.currentTimeMillis();
    for (;;) {

    if (System.currentTimeMillis() - start > 1000 * 3) // 循环3秒
    break;
    }
    }
    });
    t1.start();

    Thread.sleep(100L);// 等t1线程启动

    Thread t2 = new Thread(() -> {

    synchronized (target) {// 获取target的锁
    System.out.println("--- run ---");
    }
    });
    t2.start();

    Thread.sleep(1000L);// 等两个线程都启动
    System.out.println(t2.getState());// 此时线程t1 t2 都未结束

    }
  4. WAITING:等待状态,准确的说是死等,线程调用object.wait()方法,等待其它线程调用object.notify()或object. notifyall()方法,此时该线程进入等待状态。或调用LockSupport.park()方法线程也会进入该状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static void main(String[] args) throws InterruptedException {

    Object target = new Object();

    Thread t1 = new Thread(() -> {

    synchronized (target) {// 获取target的锁
    try {
    target.wait();// 调用wait()方法
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    t1.start();

    Thread.sleep(1000L);
    System.out.println(t1.getState());// 由于没有线程调用target.notifyAll()或target.notify(),此时线程t1一直处于等待状态,

    }
  5. TIMED_WAITING: 等待未超时,线程调用object.wait(long)方法,等待其它线程调用object.notify()或object. notifyall()方法,不过只会等待指定的时间,未超时之前该线程进入等待未超时状态。或线程进入通过sleep方法进行“睡眠”时,也会进入等待未超时状态。或调用LockSupport.parkNanos(Object, long)方法、LockSupport.parkUntil(Object, long)方法线程也会进入该状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static void main(String[] args) throws InterruptedException {

    Object target = new Object();

    Thread t1 = new Thread(() -> {

    synchronized (target) {// 获取target的锁
    try {
    target.wait(3000L);// 调用wait()方法,等待其它线程调用target.notifyAll()或target.notify(),不过只等待3秒
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    t1.start();


    Thread.sleep(1000L);
    // 由于没有线程调用target.notifyAll()或target.notify(),3秒内(此时过去了1秒)线程t1一直处于等待未超时状态
    System.out.println(t1.getState());
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {

    try {
    Thread.sleep(3000L);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });
    t1.start();

    Thread.sleep(1000L);
    // 线程t1依旧在睡眠,其处于等待未超时状态
    System.out.println(t1.getState());

    }
  6. TERMINATED:终止(死亡)状态,代码执行结束或外部干涉导致线程终止,进入死亡状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {

    System.out.println("--- run ---");
    });
    t1.start();

    Thread.sleep(1000L);//
    // 过了1秒,线程t1已经执行完,其处于死亡状态
    System.out.println(t1.getState());
    }

线程状态的获取

通过调用Thread#getState方法获取到当前线程的状态。

线程优先级

java线程优先级范围:1~10,优先级从低到高。

优先级越高,越容易得到cpu时间片。

相关方法:

  • 获取线程的优先级:Thread#getPriority
  • 设置线程优先级:Thread#setPriority

守护线程

  • java中线程分为用户线程(User Thread )守护线程(Daemon Thread )
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机可以不等待守护线程执行完毕
  • 通过调用Thread#setDaemon(true)方法将线程设置为守护线程

守护线程最典型的应用就是 GC (垃圾回收器)。用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于,当用户线程全部执行结束,只剩下守护线程存在了,虚拟机也就退出了, 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

停止线程的方法

  • Thread#stop方法,过时,不建议使用

  • Thread#destroy方法,废弃

    1
    2
    3
    4
    @Deprecated
    public void destroy() {
    throw new NoSuchMethodError();
    }
  • 设置某种标志位,结束线程代码的执行,正常停止线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class MyRunnable implements Runnable {

    private volatile boolean flag = true;

    @Override
    public void run() {

    while (flag) {

    System.out.println("运行");
    //... do something
    }
    System.out.println("结束");
    }
    // 对外提供停止方法
    public void doStop() {

    flag = false;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) throws InterruptedException {

    MyRunnable myRunnable = new MyRunnable();
    new Thread(myRunnable).start();
    Thread.sleep(800);
    myRunnable.doStop();
    }

线程休眠

调用Thread.sleep方法使当前线程进入阻塞状态,线程结束休眠后进入就绪态;

线程通过Thread.sleep方法进入休眠时,不会释放对象锁

线程礼让

调用Thread.yield()方法使当前线程让出cpu时间片,使线程从运行态重新进入就绪态。值得注意的是:A线程让出cpu时间片,不代表B线程就可以获取到cpu时间片,下一次获取cpu时间片的线程可能还是A。

线程“插队”

如在线程A的代码中,调用B线程对象的Thread#join方法,将B线程加入到A线程,A线程需要等待B线程执行完之后才能继续执行自己的代码。好比插队,可传入等待时间表示允许插队的最长时间,超过该时间,B线程还未执行结束,则不再等待。

Executor框架

下面一一引出Executor框架相关的类或接口

Executor

前文我们将RunnableCallable称之为任务,那么任务有了,就要有对应的任务执行者和管理者,如果不进行管理,直接使用new Thread()的方法创建线程有很多缺点:

  • new Thread()耗费性能
  • 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源
  • 不利于扩展,比如如定时执行、定期执行、线程中断等

所以任务(线程)的管理是必须的。

对于Runnable,我们不期望有结果(无返回值),只管执行,所以Executor接口出现了。

通过Executorexecutor方法来启动任务(子线程),更加便捷,并且可以避免“this逃逸”问题

this逃逸问题

this逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发奇怪的错误。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Worker {

public Worker() {
// 在构造方法创建并启动线程
EscapeRunnable er = new EscapeRunnable();
new Thread(er).start();// 危险,构造可能未完成,但是 er 对象已经能调用正在构造的Woker对象的doWork方法
// ...
}
private void doWork() {
//... do something
}
// 内部类
private class EscapeRunnable implements Runnable {

@Override
public void run() {

Worker.this.doWork();
}
}
}

可修改如下规避this逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Worker {

private Thread thread;
public Worker() {
// 在构造方法创建创建但未启动线程
thread = new Thread(this.new EscapeRunnable());
// ...
}
// 给外部调用者提供初始化操作
public void init() {
thread.start();
}
private void doWork() {
//... do something
}
// 内部类
private class EscapeRunnable implements Runnable {

@Override
public void run() {

Worker.this.doWork();
}
}
}

ExecutorService

对于Callable,也需要有一个执行者和管理者,于是ExecutorService出现了,无论是Callable还是Runnable,这些任务都涉及管理和扩展方面的优化,ExecutorService索性直接扩展Executor增加对Callable支持。

ExecutorService提供了任务生命周期管理等功能 ,实际开发中用的更多的是ExecutorService,它的底层实现就是线程池

Future

任务提交后,我们肯定需要知道任务执行得怎么样了,而ExecutorService使用Future来跟踪任务,通过ExecutorService提交任务后就可以得到Future对象。

Future接口的主要方法如下:

  • boolean cancel(boolean mayInterruptIfRunning);

    尝试取消任务,成功则返回true,失败false(任务已经完成、已经被取消或由于其他原因无法取消)

    mayInterruptIfRunning 参数:是否中断任务,true,中断任务且取消任务,false,允许任务继续执行到结束,但是获取结果时会抛异常。

  • boolean isCancelled();

    任务是否已经取消

  • boolean isDone();

    任务是否执行结束

  • V get() throws InterruptedException, ExecutionException;

    获取任务执行结果,获取不到结果之前会一直等待,如果任务已经取消了则抛出异常

  • V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

    获取任务执行结果,但只会等待指定的时间,超时则抛出异常。

    timeout 参数:时间数量

    unit 参数:时间单位

Executors

Executors类是Executor体系相关的工具类,通过它可以很方便的操作Executor体系相关的接口示例,如创建线程池。

总结

以上就是Executor框架的主要知识点,总结一下,Executor框架要由三大部分组成:

  1. 任务(Runnable /Callable)
  2. 任务的执行(Executor)
  3. 异步计算的结果(Future)

Executor 框架的使用

主线程首先要创建实现Runnable或者Callable接口的任务对象,把任务对象交给ExecutorService执行,得到结果对象Future,最后,主线程可以通过对象Future来获取任务结果或取消任务。


本文参考B站UP主“狂神说Java”https://space.bilibili.com/95256449/的Java多线程教学视频进行整理。

感谢成长路上为在下传道受业解惑之人