Featured image of post java多线程

java多线程

本文阅读量

多线程

java是通过java.lang.Thread类的对象来代表线程的

创建

多线程创建方式1:继承Thread类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
  // 2、重写run方法,声明线程要做的事情
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println("子线程输出:"+i);
    }
  }
}

// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
  // 3、创建线程对象,代表具体的线程
  Thread t = new MyThread();
  // 4、启动线程:会自动执行线程的run方法
  // t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
  t.start();
  
  for(int i = 0;i<4;i++){
      System.out.println("主线程输出:"+i);
    }
}

方式1优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

多线程创建方式2:实现Runnable接口

 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
// 1、定义一个类实现Runnable接口
class MyRunnable implements Runnable{
  // 2、重写run方法,声明线程要做的事情
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println("子线程输出:"+i);
    }
  }
}

// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
  // 3、创建任务类的一个对象
  Runnable target = new MyRunnable();
  // 4、把任务对象交给线程对象
  // public Thread(Runnable task) 封装Runnable对象成为线程对象
  Thread t = new Thread(target);
  // 5、启动线程
  t.start();
  
  for(int i = 0;i<4;i++){
      System.out.println("主线程输出:"+i);
    }
}

优点:

任务类只是实现了接口,可以继续继承其他类、实现其他接口,扩展性强

缺点

需要多一个Runnable对象

 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
// 方式2匿名内部类实现
Runnable target = new Runnable(){
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println("子线程1输出:"+i);
    }
  }
}
Thread t = new Thread(target);
t.start();

// 进一步优化
new Thread(new Runnable(){
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println("子线程2输出:"+i);
    }
  }
}).start();

// 进一步简写
new Thread(() -> {
  for(int i = 0;i<4;i++){
      System.out.println("子线程3输出:"+i);
    }
}).start();

多线程创建方式3:实现Callable接口

前两种实现方式,无法解决run方法返回结果的问题(即:线程执行完毕返回数据)

JDK5提供了Callable接口和FutureTask类可以解决这个问题

步骤

  1. 创建任务对象
    • 定义一个类实现Callable接口,重写 call方法,封装要做的事情和要返回的数据
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  2. 把线程任务对象交给Thread对象
  3. 调用Thread对象的start方法启动线程
  4. 线程执行完毕后,通过FutureTask对象的get方法区获取线程任务执行的结果
 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 1、定义一个类实现Callable接口
class MyCallable implements Callable<String>{
  private int n;
  public MyCallable(int n){
    this.n = n;
  }
  // 2、重写call方法,声明任务和返回的结果
  @Override
  public String call() throws Exception{
    int sum = 0;
    for(int i=0;i<=n;i++){
      sum+=1;
    }
    return "子线程求和(1-"+n+")的结果是:"+sum;
  }
}

// 3、创建Callable对象
Callable<String> call = new MyCallable(100);
// 4、把Callable对象,封装成FutureTask对象
// 未来任务对象有两个作用:
//  1> 它是一个Runnable对象
//  2> 它可以获取线程执行后的结果
FutureTask<String> task = new FutureTask<>(call);
// 5、把未来任务对象交给线程对象
Thread t = new Thread(task);
// 6、启动线程
t.start();

Callable<String> call2 = new MyCallable(200);
FutureTask<String> task2 = new FutureTask<>(call2);
Thread t2 = new Thread(task2);
t2.start();

// 获取第一线程的结果
try{
  // 如果第一个线程没有执行完毕,会在这里等待第一个线程执行完毕后,再获取结果
  String rs1 = task.get();
  System.out.println(rs1);
}catch(Excetpion e){
  e.printStackTrace();
}

// 获取第二线程的结果
try{
  String rs2 = task2.get();
  System.out.println(rs2);
}catch(Excetpion e){
  e.printStackTrace();
}

优点:

线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;

可以在线程执行完毕后区获取线程执行的结果

缺点

编码复杂一点

线程常用方法

 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
32
33
34
35
36
37
38
39
// 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
  public MyThread(String name){
    super(name);
  }
  // 2、重写run方法,声明线程要做的事情
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println(Thread.currentThread().getName()+"子线程输出:"+i);
    }
  }
}

// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
  // 3、创建线程对象,代表具体的线程
   Thread t1 = new MyThread("1号线程"); // 通过有参构造器为线程命名
  // Thread t1 = new MyThread();
  // public void setName(String name) 为线程起名字
  // t1.setName("1号线程");
  // 4、启动线程:会自动执行线程的run方法
  // t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
  t1.start();
  System.out.println(t1.getName());// 默认Thread-0
  
  Thread t2 = new MyThread();
  t2.setName("2号线程");
  t2.start();
  System.out.println(t2.getName());// 默认Thread-1
  
  // 这个代码是哪个线程在执行,就会得到哪个线程对象
  Thread m = Thread.currentThread();
  System.out.println(m.getName()); // main
  
  for(int i = 0;i<4;i++){
      System.out.println(m.getName()+"线程输出:"+i);
    }
}

线程休眠

1
2
3
4
5
for (int i=0;i<=5;i++){
  System.out.println("输出:"+i);
  // 作用:让当前线程执行的慢一点
  Thread.sleep(1000); // 休眠1s后再执行
}

join线程

 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
 // 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
  // 2、重写run方法,声明线程要做的事情
  @Override
  public void run(){
    for(int i = 0;i<4;i++){
      System.out.println("子线程输出:"+i);
    }
  }
}

// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
  // 3、创建线程对象,代表具体的线程
  Thread t = new MyThread();
  // 4、启动线程:会自动执行线程的run方法
  // t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
  t.start();
  
  for(int i = 0;i<4;i++){
    System.out.println("主线程输出:"+i);
  	if(i == 2){
      t.join(); // 让t插队先执行完毕!在继续执行主线程后面的程序
    } 
  }
}

线程安全

是什么

多个线程,同时操作同一个共享资源时,可能会出现业务安全问题

出现原因

  1. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

模拟线程安全问题

 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
32
33
34
35
36
37
38
39
40
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private String cardId;
    private double money;

    // 取钱
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        if(this.money >= money) {
            System.out.println(name + "取钱成功,吐出" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name+"取钱成功后,账户余额:" + this.money);
        }else{
            System.out.println(name + "来取钱:余额不足");
        }
    }
}


public class DrawThread extends Thread {
    private Account acc;
    public DrawThread(String name,Account acc) {
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        acc.drawMoney(10000);
    }
}

// 1、创建一个账户对象,两个人共享
Account acc = new Account("ICBC-100",10000);
// 2、创建两个线程代表小明小红,同时去同一个账户取钱
new DrawThread("小明",acc).start();
new DrawThread("小红",acc).start();

线程同步

解决线程安全的方案

思想

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

方案

原理:每次只允许一个线程加锁,加锁后才能进行访问,访问完毕后自动解锁,其他线程才能再加锁进来

同步代码块

作用:把访问共享资源的核心代码给上锁,以此保证线程安全

1
2
3
synchronized(同步锁){
  访问共享资源的核心代码
}

原理

每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

取钱案例修复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private String cardId;
    private double money;

    // 取钱
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        synchronized (this) { // 使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
            if(this.money >= money) {
                System.out.println(name + "取钱成功,吐出" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name+"取钱成功后,账户余额:" + this.money);
            }else{
                System.out.println(name + "来取钱:余额不足");
            }
        }
    }
}

锁对象使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

同步方法

作用:

把访问共享资源的核心方法给上锁,以此保证线程安全

格式

1
2
3
修饰符 synchronized 返回值类型 方法名称(形参列表){
  操作共享资源代码
}

原理

每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

底层原理

隐式锁对象,只是锁的范围是整个方法代码

取钱案例修复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public synchronized void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        if(this.money >= money) {
            System.out.println(name + "取钱成功,吐出" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name+"取钱成功后,账户余额:" + this.money);
        }else{
            System.out.println(name + "来取钱:余额不足");
        }
    }

只需要给drawMoney加锁synchronized就可以了,推荐使用同步代码块,因为同步方法锁的范围更广

锁方法使用规范

  • 如果方法是实例方法:同步方法默认使用this作为锁对象
  • 如果方法是静态方法:同步方法默认使用类名.class作为锁对象

Lock锁

Lock锁是JKD5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象

 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
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private String cardId;
    private double money;
    private Lock lock = new ReentrantLock();

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    // 取钱
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        try {
            lock.lock(); // 加锁
            if (this.money >= money) {
                System.out.println(name + "取钱成功,吐出" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱成功后,账户余额:" + this.money);
            } else {
                System.out.println(name + "来取钱:余额不足");
            }
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺

常见模型

生产者与消费者模型

生产者线程负责生产数据

消费者线程负责消费生产者生产的数据

注意:

生产者生产完数据应该等待自己,通知消费者消费

消费者消费完数据也应该等待自己,再通知生产者生产

案例

notify,notifyAll,wait属于Object类

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 桌子
@Data
public class Desk {
    private String data; // 包子数据
    // 消费包子
    public synchronized void get() throws Exception {
        String name = Thread.currentThread().getName();
        if(data==null) {
            // 没有包子,暂停自己,唤醒别人
          	// void notify() // 唤醒正在等待的单个线程
          	// void notifyAll() // 唤醒正在等待的所有线程
            this.notifyAll();
          	// void wait() // 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法
            this.wait();
        }else{
            // 有包子,开始消费
            System.out.println(name + "吃了" + data);
            data = null;
            this.notifyAll();
            this.wait();
        }
    }

    public synchronized void put() throws Exception {
        String name = Thread.currentThread().getName();
        if(data==null) {
            // 没有包子,做包子
            data = name + "做的包子";
            System.out.println(name + "生产了一个包子");
            this.notifyAll();
            this.wait();
        }else{
            // 有包子
            this.notifyAll();
            this.wait();
        }
    }
}

// 生产者
public class MakeThread extends Thread{
    private Desk desk;
    public MakeThread(Desk desk, String name) {
        super(name);
        this.desk = desk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                desk.put();
            } catch (Exception e) {
               e.printStackTrace();
            }
        }
    }
}
// 消费者
public class ConsumerThread extends Thread {
    private Desk desk;
    public ConsumerThread(Desk desk, String name) {
        super(name);
        this.desk = desk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                desk.get();
            } catch (Exception e) {
               e.printStackTrace();
            }
        }
    }
}

//测试
public static void main(String[] args) {
        // 1、竞争一个桌子
        Desk desk = new Desk();

        // 2、创建3个生产者线程
        new MakeThread(desk,"生产1").start();
        new MakeThread(desk,"生产2").start();
        new MakeThread(desk,"生产3").start();

        // 3、创建2个消费者线程
        new ConsumerThread(desk,"消费1").start();
        new ConsumerThread(desk,"消费2").start();
    }

线程池

就是一个可以复用线程的技术

不使用线程池

用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

线程池工作原理

线程池包含了一个任务队列(任务队列WorkQueue)和线程容器(工作线程WorkThread),线程容器中存储了固定线程,当队列中来了一个任务,就会从线程容器里获取一个线程来处理任务,当线程容器里的线程都被使用,这时队列里的任务就会等待,直到线程容器中有空闲的线程,才会继续处理任务队列里的任务

创建线程池

JDK5.0开始提供了代表线程池的接口:ExcecutorService

获取线程池对象方式

  1. 使用ExcecutorService的实现类ThreadPoolExcecutor自创建一个线程池对象
  2. 使用Excutors(线程池工具类)调用方法返回不同特点的线程池对象

ThreadPoolExcecutor构造器讲解

1
public ThreadPoolExcecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExcecutionHandler handler)
  • 参数一(corePoolSize):指定线程池的核心线程数量 正式工:3人
  • 参数二(maximumPoolSize):指定线程池的最大线程数量 最大员工数:5人(临时工:2人)
  • 参数三(keepAliveTime):指定临时线程的存活时间 临时工空闲多久被开除
  • 参数四(unit):指定临时线程存活的时间单位(秒、分、时、天)
  • 参数五(workQueue):指定线程池的任务队列 客人排队的地方
  • 参数六(threadFactory):指定线程池的线程工厂 负责招聘员工的(hr)
  • 参数七(handler):指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) 忙不过来咋办?
    1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExccutionException异常(默认策略)
    2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)
    3. ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
    4. ThreadPoolExecutor.CallerRunsPolicy:由主线程负责调用任务的run()方法从而绕过线程池直接执行

临时线程什么时候创建

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

什么时候会开始拒绝新任务

核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会开始拒绝任务

创建方式1

使用ExcecutorService的实现类ThreadPoolExcecutor自创建一个线程池对象

 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
32
33
34
35
36
37
38
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            System.out.println(Thread.currentThread().getName()+"线程进入休眠!~");
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

public static void main(String[] args) {
        // 1、创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

        // 2、 处理任务
        Runnable target = new MyRunnable();
        // void execute(Runnable command)
        pool.execute(target); // 自动创建线程,并处理此任务
        pool.execute(target); // 自动创建线程,并处理此任务
        pool.execute(target); // 自动创建线程,并处理此任务
        pool.execute(target); // 复用线程,线程设置的是3个
        pool.execute(target); // 复用线程,线程设置的是3个
        pool.execute(target); // 复用线程,线程设置的是3个
        pool.execute(target); // 到了临时线程的时机了
        pool.execute(target); // 到了临时线程的时机了
        pool.execute(target); // 到了任务拒绝策略了!


        // 3、线程池没有死亡
//        pool.shutdownNow(); // 立即关闭线程,不管任务是否完成!返回没有执行完的任务
//        pool.shutdown();// 等待全部任务执行完毕后,再关闭
    }
 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MyCallable implements Callable<String> {
    private int n;
    MyCallable(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "求和1~"+n+"的结果是:" + sum;
    }
}


public static void main(String[] args) {
        // 1、创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

        // 2、 处理任务
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));

        try {
            String s = f1.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }

        try {
            String s = f2.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }


        try {
            String s = f3.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }

    }

创建方式2

使用Excutors(线程池工具类)调用方法返回不同特点的线程池对象

Excutors线程池工具类,提供了很多静态方法用于返回不同特点的线程池对象

方法名称 说明
public static ExceutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
public static ExceutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
public static ExceutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s,则会被回收掉
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或定期执行任务
 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) {
        // 1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(3); // 就更改了这一行代码
        // 2、 处理任务
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));

        try {
            String s = f1.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }

        try {
            String s = f2.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }


        try {
            String s = f3.get();
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }

    }

Executors使用可能存在的陷阱

大并发系统环境中使用Exceutors,如果不注意可能会出现系统风险

并发&并行

进程

正在运行的程序(软件)就是一个独立的进程

线程

线程属于进程的,一个进程可以同时运行多个线程

进程中的多个线程其实是并发并行执行的

并发

进程中的线程是由cpu负责调度执行的,但cpu能同时处理线程的数量是有限的,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发

并行

在同一时刻,同时有个多个线程在被cpu(电脑核数)调度执行

线程生命周期

线程主要有6种状态

  1. new(新建)
  2. Runnable(可运行)
  3. Teminated(被终止)
  4. Blocked(锁阻塞)
  5. Waiting(无限等待)
  6. Timed Waiting(计时等待)
使用 Hugo 构建
主题 StackJimmy 设计