Java Thread 介绍


线程是操作系统调度的最小单元, 也叫轻量级进程。它被包含在进程之中, 是进程中的实际运作单位。
同一进程可以创建多个线程, 每个线程都有自己独立的一块内存空间, 并且能够访问共享的内存变量。

1 线程的分类

在 Java 中, 线程可以分为 2 种

  1. 守护线程: 守护线程是为用户线程服务的线程, 在后台默默地完成一些系统性的服务, 如垃圾回收等
  2. 用户线程: 真正完成业务的工作线程

在一个应用程序中, 如果用户线程全部结束了, 意味着程序需要完成的业务操作已经结束, 系统可以退出了。
所以当系统只剩下守护进程的时候, Java 虚拟机会自动退出。
反之, 如果程序中的用户进程还在执行中, Java 虚拟机会等待器执行完成才结束。

2 线程的创建

2.1 继承 Thread 类, 重写 run 方法

// 定义自己的线程逻辑
public class MyThread extend Thread {
    @Override
    public void run() {
        System.out.println("Thread is running " + Thread.currentThread().getName());
    }
}

// 启动线程
new MyThread().start()

2.2 实现 Runnable 接口

public void createThread() {
    // 定义自己的线程逻辑
    Thread thread = new Thread(new Runnable(){
        @Override
        public void run() {
            System.out.println("Thread is running " + Thread.currentThread().getName());
        }
    });

    thread.start();
}

2.3 实现 Callable 接口

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

Callable 接口和 Runnable 接口类似, call 方法里面就是需要线程执行的逻辑, 不同的是 Callable 有返回值
同时还有一个隐藏的不同点, Callable 内部可以抛出异常, 同时这个异常是可以被捕获的, 所以可以通过异常再做一次容错处理。

public void createThread() {
    // 实现 Callable 实现线程
    Callable<String> callable = new Callable<>() {
        @Override
        public String call() throws Exception {
            System.out.println("Thread is running " + Thread.currentThread().getName());
            Thread.sleep(3000L);
            return "finish";
        }
    };

    // 这里借助线程池来实现线程
    ExecutorService executorService = Executors.newSingleThreadExecutor();

    // 提交任务到线程池, Future 是对执行结果的封装
    Future<String> future = executorService.submit(callable);

    try {
        // 尝试获取执行结果
        // 注意: Future.get 方法是一个阻塞方法。如果对应的线程这时候还没有执行完成, 调用这个方法, 会阻塞当前线程
        String result = future.get();
        System.out.println("Thread's result:" + result);

    } catch(Exception e) {
        e.printStackTrace();
    } finally {
        // 关闭线程池
        executorService.shutdown();
    }    
}

2.4 创建 FutureTask 实例

FutureTask 的 UML 图:
Alt 'FutureTask 的 UML 图'

可以发现: FutureTask 还实现了 Runnable 接口, 所以可 FutureTask 也可以当做 Runnable 使用。
同时其还实现了一个 Future 的接口。

Futrue 接口的定义如下

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

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

Futrue 主要是用于子线程将自己的执行结果返回给主线程:

  1. 主线程将任务提供给子线程时, 会立即返回一个 Future 的返回值
  2. 一开始这个对象的返回值会是空的, 后续子线程执行完成后, 会把返回值翻到这个对象内
  3. 主线程就可以主动通过这个对象获取到执行结果
  4. 因为主线程主动获取执行结果时, 可能子线程还未执行完成, 所以获取返回结果的方法是阻塞的, 主动获取返回结果时, 如果还未有返回值时, 主线程将会被阻塞等待到有返回结果

通过 Future 的声明, 可以知道 Future 除了获取返回结果外, 还具备了取消任务, 获取任务是否完成等功能。

public void createThread() {
    
    Callable<String> callable = new Callable<>() {
        @Override
        public String call() throws Exception {
            System.out.println("Thread is running " + Thread.currentThread().getName());
            Thread.sleep(3000L);
            return "finish";
        }
    };

    // 创建 FutureTask 实例
    FutureTask<String> futureTask = new FutureTask<String>(callable);

    // 创建线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    
    // FutrueTask 执行方式一
    // 此处可以不要返回值, 通过入参的 futureTask 获取执行结果
    Future<?> future = executorService.submit(futureTask);

    // FutureTask 执行方式二, 当 Runnable 使用
    // new Thread(futureTask).start();

    try {
        // 同样的会阻塞当前线程
        // 通过 FutureTask 创建的, 返回结果放在自身上, 不在 executorService.submit 的返回值
        // 通过 future 和 futureTask 都会阻塞当前线程
        String result = futureTask.get();
        System.out.println("Thread's result:" + result);
        
    } catch (Exception e) {
        e.printStackTrace();
    } finish {
        executorService.shutdown();
    }
}

4 种方式的对比

  1. Thread: 编写简单, 但是不能再继承其他父类, 其内部实际还是通过 Runnable 实现的
  2. Runnable: 多个线程可以共用一个 Runnable 对象, 适合多线程处理同一个任务的情况。没法知道线程的执行情况
  3. Callable: 具备和 Runnable 一样的优势的同时, 可以知道线程的执行情况。但是需要借助 ExecutorService, 没法指定工作线程执行
  4. FutureTask: 具备了 Runnable 和 Callable 的所有优点, 缺点就是编写复杂

3 线程的属性

3.1 tid

线程 Id, 用于标记不同的线程 (某个编号的线程结束后, 这个编号可能别后续创建的线程使用)。

3.2 name

线程名称, 面向人的一个属性, 用于区分不同的属性, 默认为 Thread-数字。 在实际开发中, 尽可能的自定义自己的线程名, 这样在后续的问题定位排查有帮助
(同时记得不要重复, Java 允许存在线程名相同的情况, 但是这会影响到后面问题的定义)。

3.3 priority

用于系统的线程调度用的, 表示希望某个线程能够优先得到运行。Java 定义了 1 - 10 个级别, 值越大, 优先级越高, 默认为 5。在实际使用中, 尽可能的不要自定义优先级, 可能会出现意想不到的问题, 比如线程饥饿。

3.4 daemon

是否为守护线程。一个线程是用户线程还是守护线程, 通过这个属性进行区分。
true: 表示这个线程为守护线程, 否则为用户线程, 这个属性的设置需要在线程启动之前进行设置才有效, 默认为 false, 用户线程。

3.5 threadStatus

线程状态, 标识当前线程的处于什么样的一样状态, 具体的取值后面分析。

其实 Thread 身上还有其他几个属性, 基本不是什么重要的属性, 就不展开了。

4 线程状态

4.1 状态取值

线程状态 说明
NEW 初始状态。线程已创建, 但是未启动, 既未调用 start 方法
RUNNABLE 运行状态。她包括 2 个状态: 准备状态 和 运行状态
BLOCKED 阻塞状态。线程阻塞于锁 或者 发起了阻塞式 I/O 操作 (Socket 读写)
WAITING 等待状态。当前线程需要等待其他线程执行一下特定的操作(通知, 中断)
TIME_WAITING 超时等待状态。和 WAITING 类似, 区别就是这个状态的等待是有时间限制的
TERMINATED 终止状态。线程的需要执行的任务已完成。

4.2 线程状态的转换

如图:
Alt '线程状态转换'

4.2.1 New

通过 new Thread() 创建出 Thread 实例, 实例的默认的状态就是 New

4.2.2 Runnable

Java 中的 Runnalbe 状态实际可以再细分为 Ready 和 Running。线程处于 Runnable 不一定就是在执行中的, 也有可能是在 Ready 中,
具体什么时候从 Ready 变为 Running, 完全取决于系统的调度。

4.2.3 Waiting

等待中状态, 处于等待状态的线程, 正在等待其他线程去执行一个特定的操作。

从 Runnable 转到 Waiting 的方式有

  1. Object.join()
  2. Ojbect.wait()
  3. Lock.lock(), 尝试获取锁, 获取锁失败时
  4. LockSupport.park()

从 Waiting 转到 Runnable 的方式有

  1. Object.notify()
  2. Ojbect.notifyAll()
  3. LockSupport.uppark(Thread)

4.2.4 Time_Waiting

带超时时间的等待状态。

从 Runnable 转到 Timed_waiting 的方式有

  1. Thead.sleep(long)
  2. Object.wait(long)
  3. Thread.join(long)
  4. Lock.tryLock(long, TimeUnit)
  5. LockSupport.parkNanos()
  6. LockSupport.parkUntil()

从 Timed_Waiting 转到 Runnable 的方式有

  1. Object.notify()
  2. Ojbect.notifyAll()
  3. LockSupport.uppark(Thread)

4.2.5 Blocked

阻塞状态, 此时线程无任何的处理能力

从 Runnable 转到 Blocked 的方式有

  1. 获取 synchronized 锁失败

从 Blocked 转到 Runnable 的方式有

  1. 获取 synchronized 锁成功

备注(待考证):
WAITING 和 BLOCKING 之间也存在着转换, 当多个线程阻塞于同一个锁时, 他们都处于 WAITING 状态, 当有一个线程释放锁了, 上面的线程会同时争取锁, 争取到锁的线程会进入到 RUNNABLE, 没有争取到的会进入到 BLOCKED。

4.2.6 Terminated

终止状态。
线程中的业务业务代码执行完成, 结束逻辑。

5 线程的一些基本操作

5.1 sleep

sleep(long mills) 是 Thread 的一个静态方法。
可以让当前线程进入休眠, 休眠的时间由指定的参数决定。

调用这个方法会导致线程状态变为 Timed_waiting。

5.2 wait

wait() / wait(long mills) 是 Object 的一个方法, 可以让执行这个方法的线程暂停 (进入到 Waiting / Timed_waiting)。
wait() / wait(long mills) 在使用之前需要先获取到锁, 才能进入暂停。即只能在同步代码块中使用, 同时内部要调用代码块锁住的对象的 wait()


Object lock = new Object();
Object lock2 = new Object();

// 没有在代码块中, 抛异常
//lock.wait();

// 锁住了 lock 对象
synchronized (lock) {
    try {

        // 没有锁住 lock2, 调用 lock2.wait 方法会抛异常
        //lock2.wait();

        // 正常沉睡
        lock.wait();
    } catch(InterruptedException e) {
        e.printStackTrace();
    }
}

wait 和 sleep 的区别

  1. wait 是 Object 的一个方法, sleep 是 Thread 的一个静态方法
  2. wait 需要在获取到对应的锁的时候才能使用(也就是在同步代码块, 或者同步方法内), sleep 则不需要
  3. wait 方法在执行时, 会释放自身的拥有的锁, 而 sleep 如果拥有锁, 则不会释放
  4. wait(long mills) / sleep 方法会在指定的休眠时间达到后, 重新运行。但是 wait() 方法需要其他线程调用对应的锁对象的 notify() 或者 notifyAll() (这 2 个方法也都是需要先获取到对应的锁), 进行通知后, 才有可能继续执行 (有可能同时多个线程在等待, 但是锁只有一个, 只能在等待的线程中选择一个进行唤醒)

5.3 join

join() / join(long mills) 是 Thread 的一个方法。主要用于让当前线程等待指定的线程执行完成。


Thread waitThread = new Thread(() -> {
    try {
        Thread.sleep(30000L);
        System.out.println("son thread finish");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
waitThread.start();


try {
    // 当前线程(主线程)进入暂停状态, 等待 t 线程执行完。
    waitThread.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("main thread finish");

5.4 yeild

yeild() 是 Thread 的一个静态方法。
作用: 使当前线程主动放弃其对处理器的占用, 这可能导致当前线程被暂停。这个方法是不可靠的, 有可能调用之后, 前线程还在继续执行。

5.5 interrupt

interrupt() 是 Thread 的一个方法, 调用这个方法, 可以向指定的线程发送一个信号, 让其终止, 但是最终是否能够终止, 由线程内部决定。

原理:

  1. 线程内部维护了一个 isInterrupted 的变量 (这个变量不在 Java 代码里面维护, 而是在 JVM 的代码里面), 取值范围为 0 (false), 1 (true)
  2. 调用线程的 interrupt() 方法, 会把这个标志符设为 1
  3. 当线程的状态从 Runnable 变为其他的状态时, 检测到这个标识为 1, 就会抛出 InterruptedException 异常, 同时把标志重新恢复为 0
  4. 线程的 wait/sleep/join 等方法, 都可以改变线程的状态

复位 (中断标识从 true 恢复回 false):

  1. 可以直接调用 Thread 的 静态方法 interrupted() 可以将中断标志恢复为 false.
  2. 线程抛出 InterruptedException 异常。

  目录