跳转到内容

线程

Java 线程的 API、生命周期及状态转化、线程调度、用户线程与守护线程、优先级、线程通信、源码

线程的创建方式

java 中创建单线程有三种方式:

1. 继承 Thread 类
java
// 创建
class MyThread extends Thread{
    @Override
    public void run(){
      // do something
    }
}

// 使用说明:
//  1. 继承Thread类,重写run方法;
//  2. 使用时,直接构造一个实例对象,然后调用start方法进行启动;
MyThread m = new MyThread();
m.start();
2. 实现 Runnable 接口
java
// 实现 Runnable 接口中的run方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("当前线程:"+Thread.currentThread().getId());
    }
}

// 使用说明:
//  1. 实现Runnable接口,并实现run方法;
//  2. 使用时,直接构造Thread对象,然后调用start方法进行启动;
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start(); //启动线程
3. 实现 Callable 接口
java
// 实现Callable接口中的call方法
public class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("运行结果:"+i);
        return i;
    }
}

// 使用说明:
//  1. 实现Callable接口中的call方法,并且Callable接口带有一个泛型参数,这个泛型参数表示:返回值的类型;
//  2. 使用时,先构造一个FutureTask对象来接收返回值,之后构造一个Thread对象,最后调用start方法进行启动;
//  3. 获取结果时,通过FutureTask对象的get方法进行获取,获取过程为阻塞获取;
FutureTask<Integer> future = new FutureTask<>(new MyCallable());
new Thread(future).start();
System.out.println("结果为: "+task.get());

除此以外,还有线程池的两种方式:

4. 原生线程池的方法 这种方式需要设置各种参数。
5. 静态工厂创建线程池 JDK给程序员提供的简化版线程池创建方式。
  • 注意:
    • run 方法中执行的代码一般被成为“任务”;
    • 线程的使用都是通过构造 Thread 对象来完成的
  • 比较异同:
    • 方式 1 和方式 2 都不能获取返回值;
    • Java 支持单继承和多实现,所以方式 2 可以避免单继承的局限性;
    • 方式 3 可以获取任务返回值,并且可以捕获任务执行过程中抛出的异常;
    • 方式 1、2、3 都不能控制资源,因为如果有多项任务,就要开多个线程,这就相当于“公司有一项任务就要专门招一个员工来干活儿”,而方式 4 可以控制资源,这种方式就相当于“公司固定几个员工,公司里的各项任务让这些员工依次完成”;实际开发中,方式 4 才是最常用的;

API

  • currentThread 线程名称的设置与获取
  • start 的执行顺序与线程的执行顺序不一致
  • currentThread 方法用来获取当前执行线程的名称
  • isAlive 用来判断线程是否还在存活状态,活动状态就是线程已经启动且运行没有结束。线程处于正在运行或准备开始运行的状态,就认为线程是『存活』的状态
  • sleep 让当前线程睡眠,让出 CPU,使线程从执行状态变成限时阻塞状态。等待时间结束后,线程不一定会立即执行,线程状态会变成可执行状态。作用是在指定的毫秒数内让当前『正在执行的线程』暂停执行。
  • getId 方法,作用是获取当前线程的唯一标识
  • 停止线程
    • stop 方法强制结束线程。stop 方法已经被作废,如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个原因是对锁定的对象进行『解锁』,导致数据得不同同步的处理,出现数据不一致性的问题。
    • 也可以使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
  • interrupt 让当前线程进入阻塞状态。调用 interrupt 方法不会真正的结束线程,在当前线程中打上一个停止的标记。Thread 类提供了 interrupted 方法测试当前线程是否中断,isInterrupted 方法测试线程是否已经中断。如果程中有 sleep 代码,不管是否进入到 sleep 的状态,如果调用了 interrupt 方法都会产生异常信息。
  • 暂停线程使用 suspend ,重启暂停线程使用 resume 方法,suspend 如果独占公共的同步对象,使其它线程无法访问公共同步对象,suspend 会造成共享对象数据不同步
  • join 多个线程进行合并,本质是:线程 a 需要在合并点进行等待,一直等到 b 线程执行完,或者等待超时时,才继续执行。
  • yeild 此方法相当于让当前线程让出 cpu 执行权限,让 cpu 执行其他线程,但是此方法虽然会让当前线程暂停,但是不会行阻塞当前线程,只是让线程转入就绪状态,可能当前线程暂停了一下,又立即获得了 cpu 的执行权限,又开始执了。
  • 线程的优先级
    • 在操作系统中,线程可以划分优先级,优先级较高的线程得到更多的 CPU 资源,也就 CPU 会优先执行优先级较高的线程对象中的任务。设置线程优先有助于帮助『线程调度器』确定在下一次选择哪个线程优先执行。
    • 设置线程的优先级使用 setPriority 方法,优级分为 1~10 个级别,如果设置优先级小于 1 或大于 10,JDK 抛出 IllegalArgumentException。JDK 默认设置 3 个优先级常量,MIN_PRIORITY=1(最小值),NORM_PRIORITY=5(中间值,默认),MAX_PRIORITY=10(最大值)。
    • 获取线程的优先级使用 getPriority 方法。
    • 线程的优先级具有继承性,比如线程 A 启动线程 B,线程 B 的优先级与线程 A 是一样的。
    • 高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部执行完。当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关。
    • 线程的优先还有『随机性』,也就是说优先级高的线不一定每一次都先执行完成。
  • daemon 线程分为两类,守护线程和用户线程。守护线程指在用户线程执行过程中,在后台提供某些通用服务的线程。GC 线程就是守护线程。守护线程是一种特殊的线程,特殊指的是当进程中不存在用户线程时,守护线程会自动销毁。典型的守护线程的例子就是垃圾回收线程,当进程中没有用户线程,垃圾回收线程就没有存在的必要了,会自动销毁。设置守护线程必须要在调用 start 方法之前设置,否则 JDK 会产生 IllegalThreadStateException
  • wait 挂起线程,即让线程处于阻塞状态,适用 notify 方法可以进行唤醒;
  • notify 用来唤醒被挂起的线程
  • stop 方法用来强制停止线程,目前已经处于废弃状态,因为 stop 方法可能会在不一致的状态下释放锁,破坏对象的一致性。

优先级

在 Thread 类中有一个属性专门标识了线程的优先级,默认是 5,最小值是 1,最大值是 10,数值越大,说明优先级越高。但需要注意的一点是,优先级高的线程不一定就比优先级低的线程执行的快,因为操作系统调度线程完全是随机的。具体调用哪个线程进行执行,只有天知道。

java
// jdk1.8 Thread.java line 249
public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

public final static int MAX_PRIORITY = 10;

1742544574651

线程的生命周期

java
// jdk1.8 Thread.java line 1747
public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

jdk1.8源码

Thread 类中有一个内部枚举类 State, public static enum Thread.State extends Enum<Thread.State> 。标识了线程从新建到消亡的各个阶段的状态。用一个表格来做个总结:

状态说明
NEW当线程被创建后,如通过new Thread(),它处于新建状态。此时,线程已经被分配了必要的资源,但还没有开始执行。
RUNNABLE当调用线程的start()方法后,线程进入可运行状态。在这个状态下,线程可能正在运行也可能正在等待获取 CPU 时间片,具体取决于线程调度器的调度策略。
BLOCKED线程在试图获取一个锁以进入同步块/方法时,如果锁被其他线程持有,线程将进入阻塞状态,直到它获取到锁。
WAITING线程进入等待状态是因为调用了如下方法之一:Object.wait()LockSupport.park()。在等待状态下,线程需要其他线程显式地唤醒,否则不会自动执行。
TIMED_WAITING当线程调用带有超时参数的方法时,如Thread.sleep(long millis)Object.wait(long timeout)LockSupport.parkNanos(),它将进入超时等待状态。线程在指定的等待时间过后会自动返回可运行状态。
TERMINATED当线程的run()方法执行完毕后,或者因为一个未捕获的异常终止了执行,线程进入终止状态。一旦线程终止,它的生命周期结束,不能再被重新启动。

摘自-周志明《深入理解Java虚拟机》

摘自-周志明《深入理解Java虚拟机》

  • [ ] 阻塞状态和等待状态以及限时等待状态的区别

通信和协作

什么是线程间的通信

Java 中实现线程间通信的方式有哪些?

Java 中实现线程间传递信息的方式有多种,比如说:

  1. 使用 volatile 和 synchronized 关键字修饰共享对象;
    1. volatile 可以用来修饰成员变量,告知程序任何对该变量的访问均需要从共享内存中获取,并同步刷新回共享内存,保证所有线程对变量访问的可见性。
    2. synchronized 可以修饰方法,或者同步代码块,确保多个线程在同一个时刻只有一个线程在执行方法或代码块。
  2. 使用 wait()notify() 方法实现生产者-消费者模式;
  3. 使用 Exchanger 进行数据交换;
  4. 使用 Condition 实现线程间的协调;
  5. 任务编排;

线程字节码

Thread 类的字节码
java
{{#include ./include/Thread-bytecode.log}}

make it come true