Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

    • 多线程
      • 进程与线程
        • 进程
        • 线程
        • 二者对比
      • 并行与并发
      • 创建和运行线程
        • Thread
        • Runnable
        • Thread 与 Runnable 的关系
        • Callable
        • FutureTask 源码解析
        • FutureTask 注意事项
        • get()
        • isDone()
        • 线程池
      • 查看进程线程的方法
        • Windows
        • Linux
        • Java
      • 原理之线程运行
        • 栈与栈帧
        • 线程上下文切换(Thread Context Switch)
      • 生命周期
        • 五种状态
        • 六种状态
      • 线程方法
        • 设置和获取线程名称
        • start 与 run
        • run
        • start
        • sleep 与 yield
        • sleep
        • yield
        • 线程优先级(setPriority)
        • join
        • 等待多个结果
        • 有时效的join
        • interrupt
        • 打断 sleep,wait,join 的线程
        • 打断正常运行的线程
        • 模式之两阶段终止
        • 两阶段终止模式
        • interrupt
        • 利用停止标记
        • 打断 park 线程
        • 用户线程和守护线程
        • 不推荐的方法
    • CompletableFuture
    • 锁
    • LockSupport与线程中断
    • Java内存模型之JMM
    • Volatile与Java内存模型
    • CAS
    • 原子操作类
    • ThreadLocal
    • Java对象内存布局和对象头
    • Synchronized与锁升级
    • AbstractQueuedSynchronizer之AQS
    • ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
    • JUC总结
    • 并发集合
  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • JUC
Iekr
2023-12-03
目录

多线程

# 多线程

从 start 一个线程说起,Java 线程理解以及 openjdk 中的实现

private native void start0();
1

Java 语言本身底层就是 C++ 语言,OpenJDK 源码网址 http://openjdk.java.net/ openjdk8\hotspot\src\share\vm\runtime

更加底层的 C++ 源码解读

openjdk8\jdk\src\share\native\java\lang\thread.c

java 线程是通过 start 的方法启动执行的,主要内容在 native 方法 start0 中,Openjdk 的写 JNI 一般是一一对应的,Thread.java 对应的就是 Thread.c,start0 其实就是 JVM_StartThread。此时查看源代码可以看到在 jvm.h 中找到了声明,jvm.cpp 中有实现。

image-20231203190211783

openjdk8\hotspot\src\share\vm\prims\jvm.cpp

image-20231203190229848

image-20231203190235838

openjdk8\hotspot\src\share\vm\runtime\thread.cpp

image-20231203190250833

Java 多线程相关概念

  • 进程:是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源
  • 线程:⼀个进程会有 1 个或多个线程的

# 进程与线程

# 进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

# 线程

  • 一个进程之内可以分为一到多个线程。

  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器

# 二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享

  • 进程间通信较为复杂

    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

# 并行与并发

单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 。

一般会将这种线程轮流使用 CPU 的做法称为并发, concurrent

CPU 时间片 1 时间片 2 时间片 3 时间片 4
core 线程 1 线程 2 线程 3 线程 4

image-20231226053746956

多核 cpu 下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

CPU 时间片 1 时间片 2 时间片 3 时间片 4
core1 线程 1 线程 2 线程 3 线程 4
core2 线程 4 线程 4 线程 2 线程 2

image-20231226053851228

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力 。

  • 并行(parallel)是同一时间动手做(doing)多件事情的能力。

# 创建和运行线程

# Thread

// 创建线程对象
Thread t = new Thread() {
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();
1
2
3
4
5
6
7
8

# Runnable

使用 Runnable 配合 Thread,把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
    public void run(){
        // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start(); 
1
2
3
4
5
6
7
8
9

也可以使用 lambda 精简代码

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
1
2
3
4
5

# Thread 与 Runnable 的关系

分析 Thread 的源码,理清它与 Runnable 的关系

Runnable

//Runnable源码
public interface Runnable {
    public abstract void run();
}
1
2
3
4

Thread

//Thread源码(部分)
public class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;
    
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //...
        this.target = target;
       //...
    }
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

img

  • Thread 是把线程和任务合并在了一起,Runnable 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

# Callable

FutureTask 是 Future 接口的唯一的实现类,FutureTask 同时实现了 Runnable、Future 接口。

  • FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。
  • Runnable 接口不会抛出异常 Callable 接口会抛出异常

img

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
     log.debug("hello");
     return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
1
2
3
4
5
6
7
8
9
10

当然我们也可以实现 Callable 接口

/*
创建线程的方式三: 实现callable接口 ---JDK 5.0 新增
1.创建一个实现Callable接口的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建callable接口实现类的对象
4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star
6.获取callable接口中call方法的返回值
* */
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建callable接口实现类的对象
        NumThead m=new NumThead();
        //4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象

        FutureTask futureTask = new FutureTask(m);
        //5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        //FutureTask类继承了Runnable接口
        //new Runnable = futrueTask;
        new Thread(futureTask).start();

        //6.获取callable接口中call方法的返回值
        try {
            //get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
            Object sum = futureTask.get();
            System.out.println("总和是:"+sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

//1.创建一个实现Callable接口的实现类
class  NumThead implements Callable{
   // class  NumThead implements Callable<Integer>{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
    //public Integer call() throws Exception {
        int sum=0;
        for(int i=1;i<=100;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

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

# FutureTask 源码解析

有了 Runnable,为什么还要有 Callable 接口?我们假设一共有四个程序需要执行,第三个程序时间很长

  • Runnable 接口会按照顺序去执行,会依次从上到下去执行,会等第三个程序执行完毕,才去执行第四个
  • Callable 接口会把时间长的第三个程序单独开启一个线程去执行,第 1、2、4 线程执行不受影响

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务。子线程就去做其他的事情,过一会儿才去获取子任务的执行结果

img

如:

  • 老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙 买水,水买回来了放桌上,我需要的时候再去 get() 。
  • 4 个同学,A 算 1+20,B 算 21+30,C 算 31 * 到 40,D 算 41+50, 是不是 C 的计算量有点大啊,FutureTask 单起个线程给 C 计算,我先汇总 ABD, 最后等 C 计算完了再汇总 C, 拿到最终结果

FutureTask

//FutureTask源码(部分)
public class FutureTask<V> implements RunnableFuture<V> {
    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes
    
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    
    public void run() {
       //...
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        }
        //...
    }
    
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
    
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
}
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

Callable

//Callable源码
@FunctionalInterface
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;
}
1
2
3
4
5
6
7
8
9
10
11
  • FutureTask 内置了一个 Callable 对象,初始化方法将指定的 Callable 赋给这个对象。
  • FutureTask 实现了 Runnable 接口,并重写了 Run 方法,在 Run 方法中调用了 Callable 中的 call 方法,并将返回值赋值给 outcome 变量
  • get 方法就是取出 outcome 的值。

# FutureTask 注意事项

# get()
  • get() 方法建议放在最后一行,防止线程阻塞 (一旦调用了 get() 方法,不管是否计算完成都会阻塞)
  • 一个 FutureTask, 多个线程调用 call() 方法只会调用一次
  • 如果需要调用 call 方法多次,则需要多个 FutureTask
public class CallableDemo  {
    public static void main(String[] args) throws Exception{
        CallAble c=new CallAble();
        FutureTask<Integer> futureTask=new FutureTask<>(c);

        new Thread(futureTask,"线程A").start();
        new Thread(futureTask,"线程B").start();
        Integer integer = futureTask.get();
        System.out.println("integer = " + integer);
    }
}
class CallAble implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("欢迎你调用call方法");
        return 6;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
欢迎你调用call方法
integer = 6
1
2
# isDone()

isDone() 轮询,轮询的方式会消耗无畏的 CPU 资源,而且也不见得能及时地得到计算的结果。如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞。

public class FutureTaskTest {
    public static void main(String[] args) throws Exception{
        FutureTask futureTask = new FutureTask(()->{
            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"\t"+"coming......");
            return 1024;
        });
        new Thread(futureTask).start();
        //1.如果futureTask.get()放到main线程前面,会导致main线程阻塞
        //Object o = futureTask.get();

        /*Object o = futureTask.get();//不见不散,只要出现了get()方法就会阻塞
        System.out.println("不见不散,只要出现了get()方法就会阻塞,获取到的值为:"+o);*/
        //2.过时不候
//        System.out.println(Thread.currentThread().getName()+"\t"+"线程来了.....");
//        Object o2 = futureTask.get(2L, TimeUnit.SECONDS);
        //3.使用轮询
        while(true){
            if(futureTask.isDone()){
                System.out.println("使用轮询来解决,值为:"+futureTask.get());
                break;
            }else{
                System.out.println("阻塞中**********");
            }
        }
    }
}
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

# 线程池

todo

# 查看进程线程的方法

# Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程

  • tasklist 查看进程

    # tasklist | findstr 查找关键字
    tasklist | findstr java
    
    1
    2
  • taskkill 杀死进程

    # taskkill /F(彻底杀死)/PID(进程PID)
    taskkill 10001
    
    1
    2

# Linux

ps -fe # 查看所有进程 
ps -fT -p  10001 # 查看某个进程(PID)的所有线程 
kill 10001 # 杀死进程 top 按大写 H 切换是否显示线程 
top -H -p  10001 # 查看某个进程(PID)的所有线程
1
2
3
4

# Java

jps # 命令查看所有 Java 进程 
jstack # 查看某个 Java 进程(PID)的所有线程状态 
jconsole  # 来查看某个 Java 进程中线程的运行情况(图形界面)
1
2
3

jconsole 远程监控配置,需要以如下方式运行你的 java 类

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 - Dcom.sun.management.jmxremote.authenticate=是否认证 java类
1
2

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

# 原理之线程运行

# 栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

# 线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

# 生命周期

# 五种状态

这是从 操作系统 层面来描述的

image-20231226062201610

  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 运行状态:指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从运行状态转换至可运行状态,会导致线程的上下文切换
  • 阻塞状态:
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 阻塞状态
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
    • 与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

image-20231226070705246

# 六种状态

这是从 Java API 层面来描述的

根据 Thread.State 枚举,分为六种状态

image-20231226062303980

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束

image-20231226070609898

# 线程方法

线程一般拥有以下常用方法

方法 功能 说明
public void start() 启动一个新线程;Java 虚拟机调用此线程的 run 方法 start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
public void run() 线程启动后调用该方法 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
public void setName(String name) 给当前线程取名字
public void getName() 获取当前线程的名字。线程存在默认名称:子线程是 Thread - 索引,主线程是 main
public static Thread currentThread() 获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行。Thread.sleep(0) : 让操作系统立刻重新进行一次 cpu 竞争
public static native void yield() 提示线程调度器让出当前线程对 CPU 的使用 主要是为了测试和调试
public final int getPriority() 返回此线程的优先级
public final void setPriority(int priority) 更改此线程的优先级,常用 1 5 10 java 中规定线程优先级是 1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率
public void interrupt() 中断这个线程,异常处理机制
public static boolean interrupted() 判断当前线程是否被打断,清除打断标记
public boolean isInterrupted() 判断当前线程是否被打断,不清除打断标记
public final void join() 等待这个线程结束
public final void join(long millis) 等待这个线程死亡 millis 毫秒,0 意味着永远等待
public final native boolean isAlive() 线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on) 将此线程标记为守护线程或用户线程
public long getId() 获取线程长整型 的 id id 唯一
public state getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
public boolean isInterrupted() 判断是否被打 断 不会清除 打断标记

# 设置和获取线程名称

  • setName(String name) :将此线程的名称更改为等于参数 name

  • getName() :返回此线程的名称,注意:要是类没有继承 Thread, 不能直接使用 getName() ;

    • 要是没有继承 Thread, 要通过 Thread.currentThread 得到当前线程,然后调用 getName() 方法
  • currentThread() :返回对当前正在执行的线程对象的引用

image-20231226063622445

也可以通过带参构造函数设置线程名称

public class MyThread extends Thread {
    public MyThread() {}

    public MyThread(String name) {
        super(name);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":"+i);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# start 与 run

# run

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName());
            FileReader.read(Constants.MP4_FULL_PATH);
        }
    };
    t1.run();
    log.debug("do other things ...");
}
1
2
3
4
5
6
7
8
9
10
11

输出

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
1
2
3
4

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

# start

将上述代码的 t1.run() 改为 t1.start();

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName());
            FileReader.read(Constants.MP4_FULL_PATH);
        }
    };
    t1.start();
    log.debug("do other things ...");
}
1
2
3
4
5
6
7
8
9
10
11

输出

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
1
2
3
4

程序在 t1 线程运行, FileReader.read() 方法调用是异步的

结论:

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程

  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug("running...");
        }
    };
    System.out.println(t1.getState());
    t1.start();
    System.out.println(t1.getState());
}
1
2
3
4
5
6
7
8
9
10
11

输出

NEW
RUNNABLE
03:45:12.255 c.Test5 [t1] - running...
1
2
3

可以看见,start 方法创建了一个新线程,将线程从就绪状态切换为 Runnable

# sleep 与 yield

# sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug("enter sleep...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.debug("wake up...");
                e.printStackTrace();
            }
        }
    };
    t1.start();

    Thread.sleep(1000);
    log.debug("interrupt...");
    t1.interrupt();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

输出

03:47:18.141 c.Test7 [t1] - enter sleep...
03:47:19.132 c.Test7 [main] - interrupt...
03:47:19.132 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test7$1.run(Test7.java:14)
1
2
3
4
5
6
  1. 睡眠结束后的线程未必会立刻得到执行
  2. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。其底层还是 sleep 方法。
@Slf4j(topic = "c.Test8")
public class Test8 {

    public static void main(String[] args) throws InterruptedException {
        log.debug("enter");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
//        Thread.sleep(1000);
    }
}
1
2
3
4
5
6
7
8
9
10
  1. 在循环访问锁的过程中,可以加入 sleep 让线程阻塞时间,防止大量占用 cpu 资源。

# yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

# 线程优先级 (setPriority)

线程有两种调度模型

  • 分时调度模式:所有线程轮流使用 CPU 的使用权, 平均分配每个线程占有 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU, 如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些 [Java 使用的是抢占式调度模型]

线程可以通过 setPriority() 设置线程优先级,线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 。线程优先级范围是:1-10。

image-20231226063228871

线程优先级高仅仅表示线程获取的 CPU 时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果,如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。

@Slf4j(topic = "c.TestYield")
public class TestYield {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
//                Thread.yield();
                System.out.println("              ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出结果

# 优先级
---->1 283500
---->2 374389
# yield
---->1 119199
---->2 101074
1
2
3
4
5
6

可以看出,线程优先级和 yield 会对线程获取 cpu 时间片产生一定影响,但不会影响太大。

同时我们可以使用 getPriority() 方法获取线程的优先级

# join

当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (等待这个线程结束),同时可以设置等待指定的毫秒之后继续 (等待这个线程结束,如果超时指定时间则不继续等待)。

下面的代码执行,打印 r 是什么?

static int r = 0;
public static void main(String[] args) throws InterruptedException {
    test1();
}
private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        r = 10;
    });
    t1.start();
    log.debug("结果为:{}", r);
    log.debug("结束");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

分析:

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决方法:用 join 将 main 线程暂停等待 t1 线程执行结束,加在 t1.start() 之后即可

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

image-20231226064343916

# 等待多个结果

问,下面代码 cost 大约多少秒?

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test2();
}
private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        sleep(2);
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 第一个 join:等待 t1 时,t2 并没有停止,而在运行
  • 第二个 join:1s 后,执行到此,t2 也运行了 1s, 因此也只需再等待 1s

如果颠倒两个 join 呢?

最终都是输出

20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
1

image-20231226064625344

# 有时效的 join

当线程执行时间没有超过 join 设定时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

输出

20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
1

当执行时间超时,没等够时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(2);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

输出

20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502
1

# interrupt

interrupt 的本质是将线程的打断标记设为 true,并调用线程的三个 parker 对象(C++ 实现级别)unpark 该线程。

打断线程不等于中断线程,有以下两种情况:

  • 打断正在运行中的线程并不会影响线程的运行,但如果线程监测到了打断标记为 true,可以自行决定后续处理。
  • 打断阻塞中的线程会让此线程产生一个 InterruptedException 异常,结束线程的运行。但如果该异常被线程捕获住,该线程依然可以自行决定后续处理(终止运行,继续运行,做一些善后工作等等)

# 打断 sleep,wait,join 的线程

这几个方法都会让线程进入阻塞状态

打断 sleep 的线程,会清空打断状态,以 sleep 为例

private static void test1() throws InterruptedException {
    Thread t1 = new Thread(()->{
        sleep(1);
    }, "t1");
    t1.start();
    sleep(0.5);
    t1.interrupt();
    log.debug(" 打断状态: {}", t1.isInterrupted());
}
1
2
3
4
5
6
7
8
9

输出

java.lang.InterruptedException: sleep interrupted
 at java.lang.Thread.sleep(Native Method)
 at java.lang.Thread.sleep(Thread.java:340)
 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
 at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
 at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
 at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false
1
2
3
4
5
6
7
8

# 打断正常运行的线程

打断正常运行的线程,不会清空打断状态

private static void test2() throws InterruptedException {
    Thread t2 = new Thread(()->{
        while(true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if(interrupted) {
                log.debug(" 打断状态: {}", interrupted);
                break;
            }
        }
    }, "t2");
    t2.start();
    sleep(0.5);
    t2.interrupt();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出

20:57:37.964 [t2] c.TestInterrupt - 打断状态: true
1

# 模式之两阶段终止

Two Phase Termination 在一个线程 T1 中如何 “优雅” 终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

错误思路:

  • 使用线程对象的 stop() 方法停止线程 :stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程 :目的仅是停止一个线程,但这种做法会让整个程序都停止
# 两阶段终止模式

image-20231226065730886

# interrupt

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

class Test {
  public static void main(String[] args) throws InterruptedException {
    TPTInterrupt t = new TPTInterrupt();
    t.start();
    Thread.sleep(3500);
    log.debug("stop");
    t.stop();
  }
}

class TPTInterrupt {
  private Thread thread;
  public void start() {
    thread = new Thread(() -> {
      while (true) {
        Thread current = Thread.currentThread();
        if (current.isInterrupted()) {
          log.debug("料理后事");
          break;
        }
        try {
          Thread.sleep(1000);
          log.debug("将结果保存");
        } catch (InterruptedException e) {
          current.interrupt();
        }
        // 执行监控操作
      }
    }, "监控线程");
    thread.start();
  }
  public void stop() {
    thread.interrupt();
  }
}
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

结果

11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop 
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
1
2
3
4
5
# 利用停止标记
class Test {
  public static void main(String[] args) throws InterruptedException {
    TPTVolatile t = new TPTVolatile();
    t.start();
    Thread.sleep(3500);
    log.debug("stop");
    t.stop();
  }


}

// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
  private Thread thread;
  private volatile boolean stop = false;
  public void start() {
    thread = new Thread(() -> {
      while (true) {
        Thread current = Thread.currentThread();
        if (stop) {
          log.debug("料理后事");
          break;
        }
        try {
          Thread.sleep(1000);
          log.debug("将结果保存");
        } catch (InterruptedException e) {
        }
        // 执行监控操作
      }
    }, "监控线程");
    thread.start();
  }
  public void stop() {
    stop = true;
    thread.interrupt();
  }
}
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

输出

11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop 
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
1
2
3
4
5

# 打断 park 线程

打断 park 线程,不会清空打断状态

private static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.debug("park...");
        LockSupport.park();
        log.debug("unpark...");
        log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();
    sleep(0.5);
    t1.interrupt();
}
1
2
3
4
5
6
7
8
9
10
11

输出

21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true 
1
2
3

如果打断标记已经是 true, 则 park 会失效

private static void test4() {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            log.debug("park...");
            LockSupport.park();
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }
    });
    t1.start();
    sleep(1);
    t1.interrupt();
}
1
2
3
4
5
6
7
8
9
10
11
12

输出

21:13:48.783 [Thread-0] c.TestInterrupt - park... 
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.812 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
1
2
3
4
5
6
7
8
9
10

提示

可以使用 Thread.interrupted () 清除打断状态

# 用户线程和守护线程

Java 线程分为用户线程和守护线程,线程的 daemon 属性为 true 表示是守护线程,false 表示是用户线程

  • 守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
  • 用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作

如果垃圾回收线程是非守护线程,当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程

package com.atguigu.itdachang;

/**
 * @auther zzyy
 * @create 2020-07-07 15:39
 */
public class DaemonDemo
{
public static void main(String[] args)
{
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"\t 开始运行,"+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
        while (true) {

        }
    }, "t1");
    //线程的daemon属性为true表示是守护线程,false表示是用户线程
    t1.setDaemon(true);
    t1.start();
    //3秒钟后主线程再运行
    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

    System.out.println("----------main线程运行完毕");
}

}
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

当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java 虚拟机会自动退出。

设置守护线程,需要在 start () 方法之前进行

# 不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行
编辑 (opens new window)
上次更新: 2025/01/01, 10:09:39
字节码指令
CompletableFuture

← 字节码指令 CompletableFuture→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式