【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
简介: JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!

0. 前言

JUC并不是面向初学者的,并且关于JUC线程安全问题,需要接触过JavaWeb开发、JDBC开发、Web服务器、分布式框架才会遇到

所有代码基于jdk1.8,使用了 slf4j 打印日志调试更加方便、lombok简化java bean的编写、Junit测试工具单独测试类

maven父工程依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.36</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.36</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

1. 进程与线程

1.1 进程

程序由指令和数据组成的,但这些指令要运行,数据要读写,就必须将指令加载至Cpu,数据加载至内存。

而在指令运行过程中还需要用到磁盘、网络等设备。

进程就是用来加载指令、管理内存、管理IO的

当一个程序被运行,从磁盘加载这个程序的代码至内存,这就开启了一个进程

而进程就可以视为程序的一个实例。大部分的程序可以同时运行多个实例进程,但有的程序只能启动一个实例进程

好比一个程序,可以打开多个这个程序,例如:记事本,浏览器,视频播放器…

至于有的程序只能启动一个进程,像是大部分音乐软件、杀毒软件都是这样的。

1.2 线程

一个进程之内可以分为 一~多 个进程,亦可以将进程看作是由多个线程组成的。

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

Java中,线程作为最小调度单位,进程作为资源分配的最小单位。

在windows中,进程相当于一个盒子,里面装满了线程,相当于一个容器

1.3 进程和线程的对比

  1. 进程基本上相互独立,而线程存在于进程中,是进程的一个子集
  2. 进程拥有共享的资源,如内存空间、文件系统等,供其内部的线程调度、共享
  3. 进程间的通信较为复杂
  • 同一台计算机的进程通信称为IPC(Inter-process communication)
  • 不同计算机之间的进程通信,需要通过网络,并且遵守相同共同的协议(例如:Http、FTP…),才可以沟通
  1. 线程通信相对简单,因为他们共享进程内的内存。
  • 例如:多个线程可以访问同一个共享变量
  1. 线程更轻量化,线程上下文切换成本一般要比进程上下文切换低

2. 概念

2.1 并发

单核的CPU下,线程实际还是串行执行的。

在任务调度器中,将CPU的时间片(时间片最小约:15ms)分给不同的线程使用。

只是由于CPU在线程间切换速度非常块,让我们感觉是同时运行的。

  • 一般将这种**线程轮流使用CPU的做法称为并发(concurrent)**

上图即是CPU核心将任务工作在不同线程间反复运行

2.2 并行

在有多个核心情况下,每个核心都在同时调用不同线程,这种情况可以称为:并行

而多数情况下都是并发与并行一起出现的。

并发和并行一起出现?

  • 并行先出现现象:当core1调度执行完线程2后转向其他线程,其他核心执行完成对应线程后也会相应的转向调度其他线程

Rob Pike对于并发与并行的描述

  1. 并发是同一时间应对多件事情的能力
  2. 并行是统一时间动手做多件事情的能力

举个不恰当的例子:

  • 并发:一个人轮流并交替做多件事情
  • 并发、并行同时出现:多个人轮流并交替做多件事(其中肯定有人会争抢做某件事,例如这件事比较轻松,人人都想抢先做这个事情,当一人正在做这件事时,其他人想要做这件事就必须得等待)
  • 并行:多个人分别做着不同的事,彼此间互不干扰

2.3 实现

从方法调用的角度来说:

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

注意:同步在多线程中还有另外一层意思,是让多个线程步调一致

2.3.1 并发的实现

log.info("start...");
long start = System.currentTimeMillis();
long a = 1,b = 2;
for (long i = 0; i < 900000000; i++) {
    a *=b;
}
long end = System.currentTimeMillis();
log.info("now.... ms:"+(end-start));
log.info("continue....");
/////////////////////////////////////////////////////////////////
[main] INFO Sync - start...
[main] INFO Sync - now.... ms:678
[main] INFO Sync - continue....
  • 其实正常一个类的执行顺序就是同步的并发执行。
  • 如果没有其他调度机制,那么这时候只能等到for循环计算完后才会执行下一段代码

2.3.2 并行的实现

log.info("start...");
new Thread(()->{
    long start = System.currentTimeMillis();
    long a = 1,b = 2;
    for (long i = 0; i < 900000000; i++) {
        a *=b;
    }
    long end = System.currentTimeMillis();
    log.info("now.... ms:"+(end-start));
}).start();
log.info("continue....");
//////////////////////////////////////////////////////////
[main] INFO Async - start...
[main] INFO Async - continue....
[Thread-0] INFO Async - now.... ms:685
  • 这里单独开辟一个线程出来其实就是由新开辟出来的线程去单独做这件事了,那么这时候就可以继续往下执行代码了
  • 相当于,上述代码本身是一个线程A执行的流程,在线程A执行过程中新建了一个线程B来处理另外的事情,线程A将事情交由线程B完成,那么自己就可以继续向下执行

2.3.3 提高效率?

假设一个场景:需要执行3个计算,最后将计算结果汇总

int a = 1;// 执行耗时:10ms
int b = 2;// 执行耗时:5ms
int c = 3;// 执行耗时:2ms
int d = a+b+c; // 执行耗时:1ms 总计耗时:10+5+2 = 17ms

如果是串行执行,那么上述代码的执行耗时就没有问题。

而如果是多核并行,那么花费时间只取决于耗时最长的那个计算。即:10ms,最后加上汇总的时间:11ms

请注意:多核并行操作,需要在多核CPU才能提高效率,单核情况下仍然是轮流执行(串行)

  1. 单核CPU下,多线程并不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU,不至于一个线程总占用CPU,别的线程无法干活
  2. 多核CPU可以并行跑多个线程,但能否提高查询运行效率还是要分情况的
  • 有些任务,经过精心设计,将任务拆分然后并行执行,当然可以提高查询的一些效率
    但不是所有计算任务都能拆分的(参考【阿姆达尔定律】)
  • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率也没啥意义
  1. IO操作不占用CPU,只是一般拷贝文件用的是 阻塞I/O。
    使用 非阻塞I/O 或者 异步IO 优化性能就会好很多

2.3.5 JMH

MH 是 OpenJDK 团队开发的一款基准测试工具,一般用于代码的性能调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。

和 Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,而不仅限于rest api。

使用时,我们只需要通过配置告诉 JMH 测试哪些方法以及如何测试,JMH 就可以为我们自动生成基准测试的代码。我们只需要通过配置(主要是注解)告诉 JMH 测试哪些方法以及如何测试,JMH 就可以为我们自动生成基准测试的代码。

  • 那么 JMH 是如何做到的呢?

要使用 JMH,我们的 JMH 配置项目必须是 maven 项目。在一个 JMH配置项目中,我们可以在pom.xml看到以下配置。JMH 自动生成基准测试代码的本质就是使用 maven 插件的方式,在 package 阶段对配置项目进行解析和包装

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>{version}</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <finalName>${uberjar.name}</finalName>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>org.openjdk.jmh.Main</mainClass>
                    </transformer>
                </transformers>
                <filters>
                    <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </execution>
    </executions>
</plugin>

推荐教程:

3. Java 线程

3.1 创建和运行线程

3.1.1 第一种创建线程的方式(new Thread 实现 run 方法)

Thread t1 = new Thread("Thread-One") {
    @Override
    public void run() {
        log.info("new Thread...");
        super.run();
    }
};
t1.start();
/////////////////////////////////////////////////////
// [Thread-One] 代表是线程名称
[Thread-One] INFO CaR1T - new Thread...

3.1.2 第二种创建线程的方式(使用 Runnable 配合 Thread)

Runnable r1 = new Runnable(){
    @Override
    public void run() {
        log.info("new Runnable...");
    }
};
Thread t1 = new Thread(r1);
t1.setName("Thread-Two");
t1.start();
////////////////////////////////////////////////////////////////
[Thread-Two] INFO CaR2T - new Runnable...

3.1.2.1Runnable 的含义:

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)

3.1.2.2 lambda简化(1.8)

/**
     * lambda表达式简化
     */
@Test
public void test1(){
    Runnable r2 = () -> log.info("new Runnable use lambda...");
    Thread t1 = new Thread(r2,"Thread-Three");
    t1.start();
}
/////////////////////////////////////////
[Thread-Three] INFO CaR2T - new Runnable use lambda...

同样的,不论是否是lambda式,一样可以添加代码块

Runnable r3 = () -> {
    int i =0;
    i++;
    log.error("new Runnable use lambda..."+i);
};
new Thread(r3,"Thread-Four").start();
/////////////////////////////////////////////
[Thread-Four] ERROR CaR2T - new Runnable use lambda...1

3.1.2.3 原理:Thread 与 Runnable 的关系

  1. 方法1 是把线程和任务合并在了一起,方法2 是线程和任务分开了
  2. 用 Runnable 更容易与线程池等高级API配合
  3. 用Runnable 让任务类脱离了Thread继承体系,更灵活
new Thread(r3,"Thread-Four").start();
// 进入Thread 的构造函数
/**
target 参数就是线程需要执行的任务
*/
public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
// 随后一直进入 init 方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {}
// 在该方法中有这一段赋值操作
this.target = target; // 线程类,将当前需要执行的任务重新的赋值,并调用run()方法从而运行该任务
// 在run方法中,可以看到就是target这个类被执行了
private Runnable target;
public void run() {
    // 当target存在,则优先使用任务并运行
    if (target != null) {
        target.run();
    }
}

可以说,当用户创建了一个线程时,通过Runnable可以自主的选择该线程需要执行的哪个任务

对应第二点和第三点:用 Runnable 更容易与线程池等高级API配合。用Runnable 让任务类脱离了Thread继承体系,更灵活

当创建出一个Runnable对象后,你可以在类中的任何地方使用它,甚至可以通过其他方式传递到别的类中去使用

3.1.3 Future Task 配合 Thread

FutureTask 能够结构 Callable类型 的参数,用来处理有返回结果的情况

FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(()->{
    log.info("integerFutureTask....");
    return 100;
});
new Thread(integerFutureTask,"t1").start();
Integer result = integerFutureTask.get();// 注意:该方法是阻塞的
log.info("result is:{}", result);
////////////////////////////////////////////////////////
[t1] INFO CaR3T - integerFutureTask....
[main] INFO CaR3T - result is:10

上述创建FutureTask类,使用了lambda表达式,其实它被简化的就是Callable接口中的call方法

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;
}
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

如果不使用lambda表达式的写法:

new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 0;
    }
});

4. 线程运行

4.1 线程运行的现象

  • 交替执行的流程
  • 谁先谁后,不由我们控制
new Thread(()->{
    while (true){
        log.info("线程A......");
    }
},"A").start();
new Thread(()->{
    while (true){
        log.info("线程B......");
    }
},"B").start();
/////////////////////////////////////
[A] INFO TR1R - 线程A......
[A] INFO TR1R - 线程A......
[A] INFO TR1R - 线程A......
[A] INFO TR1R - 线程A......
[A] INFO TR1R - 线程A......
[B] INFO TR1R - 线程B......
[B] INFO TR1R - 线程B......
[B] INFO TR1R - 线程B......
[B] INFO TR1R - 线程B......

可以看见确实是线程交替运行的

4.2 查看进程线程的方法

4.2.1 windows

  • 任务管理器可以查看进程和线程数,也可以用杀死进程
  • tasklist查看进程
  • tasklist | findstr 具体名称

  • taskkill /PID <PID>杀死进程
  • taskkill /F /PID <PID> 强制杀死进程

14.2.2 linux

  • ps -fe 查看所有进程
  • ps -fT -p <PID\> 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写H切换是否显示线程
  • top -H -p <PID\> 查看某个进程(PID)的所有线程

4.2.3 Java

  • jps 命令查看所有 Java 线程
  • jstack <PID> 查看某个Java进程(PID)的所有线程状态

4.2.3.1 jconsole 远程监控配置

在运行窗口中,直接输入 jconsole 就可以打开监控控制台了

4.3 线程运行

4.3.1 栈与栈帧

Java Virtual Machine Stacks(Java虚拟机栈)

JVM中,由堆、栈、方法区组成,其中栈内存分配给到的就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存

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

public static void main(String[] args) {
    new Thread(()->{test1(10);},"t1").start();
    test1(5);
}
private static void test1(int x){
    int b =0;
    int c = x+b;
    log.info("c:"+c);
}

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

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

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

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

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

4.3.3 常见方法

方法名 功能说明 注意
start() 启动一个新线程,在新的线程运行run方法中的代码 start方法只是让线程进入就绪,里面代码不一定立即运行(Cpu的时间片还没分给它)。每个线程对象的start方法只能调用以此,如果调用了多次会出现
IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 呆呆线程运行结束,最多等待n毫秒
getId() 获得线程长整型的id唯一
getPriority() 获得线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级时1~10的整数,较大的优先级能提高该线程被CPU调度的几率
getState() 获得线程状态 java中线程状态时用6个枚举来表示:分别是
New
RUNNABLE
BLOCKED
WAITING
TIME_WAITING
TERMINATED
isInterrupted() 判断是否被打断 不会清除打断标记
IsAlive() 线程是否存活(还没有运行完毕)
interrupt() 打断线程 如果被打断线程正在sleep,wait,join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记
interrrupted() 判断当前线程是否被打断 会清除打断标记
currentThread() 获取当前正在执行的线程
sleep(long n) 让当前执行的线程休眠n毫秒,休眠期间让出CPU的时间片给其他线程
yield() 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

4.4 start 与 run

4.4.1 调用run

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @SneakyThrows
        @Override
        public void run() {
            log.info("t1 start:"+Thread.currentThread().getName());
            testForRun();
        }
    };
    t1.run();
    log.info("do continue......");
}
private static void testForRun(){
    long start = System.currentTimeMillis();
    long a = 1,b = 2;
    for (long i = 0; i < 900000000; i++) {
        a *=b;
    }
    long end = System.currentTimeMillis();
    log.info("now.... ms:"+(end-start));
}
////////////////////////////////////////////////////
[main] INFO StartMethod - t1 start:main
[main] INFO StartMethod - now.... ms:695
[main] INFO StartMethod - do continue......

可以看到run方法其实还是在main线程中运行,调用还是同步的

4.4.2 使用start启动

public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        @SneakyThrows
        @Override
        public void run() {
            log.info("t1 start:"+Thread.currentThread().getName());
            testForRun();
        }
    };
    t1.start();
    log.info("do continue......");
}
private static void testForRun(){
    long start = System.currentTimeMillis();
    long a = 1,b = 2;
    for (long i = 0; i < 900000000; i++) {
        a *=b;
    }
    long end = System.currentTimeMillis();
    log.info("now.... ms:"+(end-start));
}
///////////////////////////////////////////////////
[main] INFO StartMethod - do continue......
[t1] INFO StartMethod - t1 start:t1
[t1] INFO StartMethod - now.... ms:689

如果想要异步处理,还是直接使用start启动一个线程就好

run和start的区别:

  • run是直接在当前线程执行了某个线程中的任务(run方法)
  • start是创建了一个新的线程,并在新线程中调用run方法运行新线程中的任务

4.5 sleep 与 yield

4.5.1 sleep 休眠

  1. 调用sleep方法,会让当前线程从Runnable进入 Timed Waiting状态
public static void main(String[] args) {
    Thread t1 = new Thread("t1") {
        public void run() {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    };
    t1.start();
    log.info("t1 state:{}", t1.getState()); 
    // [main] INFO SleepRunning - t1 state:RUNNABLE
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    log.info("t1 state:{}", t1.getState()); 
    // [main] INFO SleepRunning - t1 state:TERMINATED
}
  1. 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时sleep方法会抛出 InterruptedException
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread("t1") {
        public void run() {
            log.info("t1 start.. now sleeping....");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.info("t1 wake up");
                e.printStackTrace();
            }
        }
    };
    t1.start();
    Thread.sleep(500);
    log.info("t1 end.. interrupt");
    t1.interrupt();// 打断睡眠
}
///////////////////////////////////////////////////
[t1] INFO SR2 - t1 start.. now sleeping....
[main] INFO SR2 - t1 end.. interrupt
[t1] INFO SR2 - t1 wake up
java.lang.InterruptedException: sleep interrupted
  at java.lang.Thread.sleep(Native Method)
  at com.renex.c2.SR2$1.run(SR2.java:15)
  1. 睡眠结束后的线程未必会立刻得到执行

当前线程并未被分配时间片,时间片还在其他线程手中执行,需要等待其他线程的任务完成后才会被分配时间片

  1. 建议使用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

4.5.2 yield 调度

  1. 调用 yield 会让当前线程从Running进入Runnable 状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  2. 具体的实现依赖于操作系统的任务调度器
Runnable r1 = ()->{
    int count = 0;
    for (;;){
        System.out.println("----> 1:\t"+count++);
    }
};
Runnable r2 = ()->{
    int count = 0;
    for (;;){
        // 使用yield调度,让当前运行线程进入Runnable状态,将优先权让给其他线程
        Thread.yield();
    System.out.println("                ----> 2:\t"+count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

截取一段输出

---> 1: 260323
----> 1:  260324
----> 1:  260325
----> 1:  260326
----> 1:  260327
----> 1:  260328
                ----> 2:  190640
                ----> 2:  190641
                ----> 2:  190642
                ----> 2:  190643
----> 1:  260329
----> 1:  260330
                ----> 2:  190644
                ----> 2:  190645
                ----> 2:  190646
                ----> 2:  190647
                ----> 2:  190648
----> 1:  260331
----> 1:  260332
----> 1:  260333
                ----> 2:  190649
                ----> 2:  190650
                ----> 2:  190651
                ----> 2:  190652
                ----> 2:  190653
                ----> 2:  190654

可以看到线程1 比 线程2 执行的次数多得多

4.5.3 线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度可以忽略它
/**
     * The minimum priority that a thread can have.
     */
public final static int MIN_PRIORITY = 1;
/**
     * The default priority that is assigned to a thread.
     */
public final static int NORM_PRIORITY = 5;
/**
     * The maximum priority that a thread can have.
     */
public final static int MAX_PRIORITY = 10;

Thread类中,默认最少线程是1,最多是10,默认5

  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
Runnable r1 = ()->{
    int count = 0;
    for (;;){
        System.out.println("----> 1:\t"+count++);
    }
};
Runnable r2 = ()->{
    int count = 0;
    for (;;){
        // 使用yield调度,让当前运行线程进入Runnable状态,将优先权让给其他线程
        //                Thread.yield();
        System.out.println("                ----> 2:\t"+count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
// 设置不同线程的优先级
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

截取一段输出来看看

----> 1:  92363
----> 1:  92364
----> 1:  92365
----> 1:  92366
                ----> 2:  88323
                ----> 2:  88324
----> 1:  92367
----> 1:  92368
----> 1:  92369
                ----> 2:  88325
                ----> 2:  88326
                ----> 2:  88327
                ----> 2:  88328
----> 1:  92370
----> 1:  92371
                ----> 2:  88329
                ----> 2:  88330
----> 1:  92372
----> 1:  92373
----> 1:  92374
----> 1:  92375
----> 1:  92376
----> 1:  92377
----> 1:  92378
----> 1:  92379
                ----> 2:  88331
                ----> 2:  88332
                ----> 2:  88333
                ----> 2:  88334
----> 1:  92380
----> 1:  92381
----> 1:  92382
----> 1:  92383

我们发现,线程间上下文切换好像并没有什么效果?因为 优先级 切换在CPU闲的时候几乎没什么用。只有在高负载情况下,这种效果才会慢慢体现出来。

4.5.4 防止CPU占用100%

在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其他程序

new Thread(()->{
    while (true){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}).start();
  • 可以使用wait或条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep适用于无需锁同步的场景

以下是在linux(1CPU,2核)环境下运行的结果

  • 空转情况下,CPU的占用也会跑满
PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND 
1334 root      20   0 2118192  23512  11460 S 99.7  4.9   0:08.48 java
  • 当使用sleep对线程间歇休眠后,可以实现大大降低CPU占用的效果
PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND 
1380 root      20   0 2118192  26008  11344 S  1.7  5.4   0:00.47 java

4.6 join方法详解

4.6.1 为什么需要join?

  • join()
    等待线程运行结束
  • join(long n)
    等待线程运行结束,最多等待n毫秒

在认识join前,先研究以下代码,看看x的值是什么。

package com.renex.c2;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
 * join方法
 */
@Slf4j(topic = "T3P")
public class T3P {
    public static void main(String[] args) {
        test();
    }
    static int x = 0;
    private static void test(){
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("内部线程开始");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.info("内部线程结束");
            x=10;
        });
        t1.start();
        log.info("x 的结果为:{}",x);
        log.info("结束");
    }
}
//////////////////////////////////////////////////
[main] INFO T3P - 开始
[Thread-0] INFO T3P - 内部线程开始
[main] INFO T3P - x 的结果为:0
[main] INFO T3P - 结束
[Thread-0] INFO T3P - 内部线程结束

如果认为是0就调入陷阱了

分析:

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

什么意思?

最简单的说法就是,当程序 线程1执行到log.info("内部线程开始");时,主线程已经执行到log.info("x 的结果为:{}",x);行了。那么这个时候主线程所得到的x值自然是0

解决方法:

  • 用sleep行吗?可以,但是需要把握好线程1结束的时间,让其在主线程执行获得x值的前面就让x值=10
  • 最好的办法是使用join方法,加在t1.start()后面
t1.start();
// 当主线程执行到这里,需要等待t1线程执行完毕,才可以继续执行
t1.join();
/////////////////////////////////////////
[main] INFO T3P - 开始
[t1] INFO T3P - 内部线程开始
[t1] INFO T3P - 内部线程结束
[main] INFO T3P - x 的结果为:10
[main] INFO T3P - 结束

4.6.2 应用同步

首先认知一个东西:

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

package com.renex.c2;
import lombok.extern.slf4j.Slf4j;
/**
 * join方法
 */
@Slf4j(topic = "T4P")
public class T4P {
    static int x = 0;
    static int x2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            x=10;
        }, "t1");
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            x2=20;
        }, "t2");
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        log.info("开始");
        t1.join();
        log.info("t1 join结束");
        t2.join();
        log.info("t2 join结束");
        long end = System.currentTimeMillis();
        log.info("r1:{},r2:{},耗时:{}",x,x2,end-start);
    }
}
///////////////////////////////////////////////////////////////////
[main] INFO T4P - 开始
[main] INFO T4P - t1 join结束
[main] INFO T4P - t2 join结束
[main] INFO T4P - r1:10,r2:20,耗时:2

这里不论t1还是t2线程休眠2秒,运行结果的耗时都是2秒

  • 因为这两个线程都是并行状态,只不过由于join方法需要等待
  • 当t1线程join时,t2线程一样已经开始执行了,只不过t2休眠的时间是2秒钟
  • 当t1线程join线程t2结束时,实际也就是耗时2秒钟,而执行到t2.join时,因为线程t1已经结束,所以只需要完成自己的休眠时间后(sleep(2;)),就可以往下运行了。

    在此过程中,t2.join()仅需要等待1s
  • 第二种情况,当t2.join()放在t1.join()前面,让其先等待
  • 其实当t2进行等待时,在它等待的2秒内,t1线程已经执行完毕了
  • 所以t2.join()执行完毕,运行t1.join()时,会直接往下运行

    在此过程中,t1.join()并不需要等待
  • 4.6.3 有时效的 join
    其实就是给一个最大的等待时间,当等待达到设定的时间还没有运行结束,那么就不再等待了
  • join(long n):等待线程结束,n 最大等待时间

4.7 interrupt 方法

4.7.1 打断 sleep、wait、join 的线程

Thread t1 = new Thread(() -> {
    log.info("sleep.........");
    try {
        Thread.sleep(3);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
log.info("打断状态:{}",t1.isInterrupted());
//////////////////////////////////////////////
[t1] INFO T1I - sleep.........
Exception in thread "t1" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
  at com.renex.c2.T1I.lambda$main$0(T1I.java:16)
  at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InterruptedException: sleep interrupted
  at java.lang.Thread.sleep(Native Method)
  at com.renex.c2.T1I.lambda$main$0(T1I.java:14)
  ... 1 more
[main] INFO T1I - 打断状态:false

这里的打断,是打断sleep状态下的线程,在sleep状态下,返回的打断状态都是为false,但其实它是有打断的。

4.7.2 打断正在运行的线程

Thread t1 = new Thread(() -> {
    while (true) {
        Thread currented = Thread.currentThread();
        boolean interrupted = currented.isInterrupted();
        if (interrupted){
            log.info("t1 线程被打断了!");
            break;
        }
    }
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
//////////////////////////////////////////////////////////////
[t1] INFO T1I - t1 线程被打断了!
Process finished with exit code 0

**注意:**当线程被打断后,并不会结束线程的运行!

  • 可以判断打断标记来执行代码流程

4.7.3 两阶段终止

大多数情况,会出现的一种场景,线程1去终止线程2。

这个时候可以使用一种设计模式:两阶段终止模式(TwoPhaseTermination)

  • 通过判断打断标记来确认线程的死亡与存活

错误思路:

  • 使用线程对象的stop()方法停止线程(这种方式已被抛弃)
  • stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁
  • 使用System.exit(int)方法停止线程
  • 目的仅是停止一个线程,但这个做法会让整个程序都停止
package com.renex.c2;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
 * 打断线程
 */
@Slf4j(topic = "T2I")
public class T2I {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination t = new TwoPhaseTermination();
        t.start();
        sleep(5000);
        t.stop();
    }
}
@Slf4j(topic = "TwoPhaseTermination")
class TwoPhaseTermination{
    private Thread monitor;
    // 启动监控
    public void  start(){
        monitor = new Thread(()->{
            while (true){
                Thread tList = Thread.currentThread();
                if (tList.isInterrupted()){
                    log.error("准备结束代码");
                    break;
                }
                try {
                    sleep(1000);
                    log.info("执行监控记录");
                } catch (InterruptedException e) {
                    /**
                     * 当出现在sleep阶段被打断,InterruptedException 会将打断标记设置为false
                     * 这就导致下一次循环监控时还是false
                     * 而这里执行了InterruptedException后肯定是已经被打断的,所以必须是true
                     * 所以这里还需要重新设置一次打断标记,这样再下一次循环中就可以检测出来
                     */
                    // 重新设置打断标记
                    tList.interrupt();
                    e.printStackTrace();
                }
            }
        },"monitor");
        // 启动
        monitor.start();
    }
    public void stop(){
        monitor.interrupt();
    }
}
/////////////////////////////////////////////////////
[monitor] INFO TwoPhaseTermination - 执行监控记录
[monitor] INFO TwoPhaseTermination - 执行监控记录
[monitor] INFO TwoPhaseTermination - 执行监控记录
[monitor] INFO TwoPhaseTermination - 执行监控记录
java.lang.InterruptedException: sleep interrupted
  at java.lang.Thread.sleep(Native Method)
  at com.renex.c2.TwoPhaseTermination.lambda$start$0(T2I.java:36)
  at java.lang.Thread.run(Thread.java:750)
[monitor] ERROR TwoPhaseTermination - 准备结束代码
    
Process finished with exit code 0

4.7.4 打断 park 线程

LockSupport.park() 是Java中用于线程休眠的一种方法,它与 Thread.sleep()Object.wait() 不同,因为它不会直接使线程进入休眠状态,而是首先检查是否有可用的许可证。如果有许可证,方法会立即返回,否则线程将进入休眠状态,直到发生以下三种情况之一:

  1. 其他线程调用 unpark() 方法
  2. 其他线程中断了当前线程
  3. 调用方法无缘无故地返回。
  • 打断park线程,不会清空打断状态
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.info("park....");
        LockSupport.park();
        log.info("unpark...");
        log.info("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();
    Thread.sleep(1);
    t1.interrupt();
}
/////////////////////////////////////
[t1] INFO T3I - park....
[t1] INFO T3I - unpark...
[t1] INFO T3I - 打断状态:true

4.8 不推荐的方法

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

方法名 static 功能说明
stop() 停止线程运行
supend() 挂起(暂停)线程运行
resume() 恢复线程运行

共同点:破坏同步代码块,容易造成对象的锁得不到释放,导致造成死锁问题

4.9 主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。

而有一种特殊的线程叫做守护线程:只要其他非守护线程运行结束了,即时守护线程的代码没有执行完,也会强制结束

  • 未设置守护线程的情形:
Thread t1 = new Thread(() -> {
    while (true){
        if (Thread.currentThread().isInterrupted()){
            break;
        }
    }
    log.error("t1 结束");
}, "t1");
t1.start();
Thread.sleep(1000);
log.error("结束");
t1.interrupt();
///////////////////////////////////////////////////
[main] ERROR T3I - 结束
[t1] ERROR T3I - t1 结束
  • 将t1设置为守护线程
Thread t1 = new Thread(() -> {
    while (true){
        if (Thread.currentThread().isInterrupted()){
            break;
        }
    }
    log.error("t1 结束");
}, "t1");
// 设置为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.error("结束");
// 不用打断,理应是继续重复的运行
// t1.interrupt(); 
//////////////////////////////////////
[main] ERROR T3I - 结束

可以看到,当主线程结束,理应继续运行的t1线程,也跟着结束了运行

注意:

  • 垃圾回收器线程就是一种守护线程
    当回收线程运行时,若程序结束了,那么回收线程自然会结束
  • Tomcat中的 AcceptorPoller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

4.10 线程的 5种状态

从操作系统层面来说,分为了以下五种状态

初始状态

  • 仅是在语言层面创建了线程对象,还未与操作系统线程关联

可运行状态(就绪状态)

  • 指该线程已被出啊感觉(与操作系统线程关联),可以由CPU调度执行

运行状态

  • 指获取了CPU时间片运行中的状态

当CPU时间片用完,会从**运行状态** 转换至**可运行状态**,会导致线程的上下文切换

阻塞状态

  • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入**阻塞状态**
  • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至**可运行状态**
  • 与可**运行状态的区别是,对阻塞状态**的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

终止状态

  • 表示线程已经执行完毕,声明周期已经结束,不会再转换为其他状态

4.11 Java API层面的六种状态

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

public enum State{
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED
}

NEW

  • 线程刚被创建,但是还没有调用start()方法

创建出来了一个线程,但是没有启动它

RUNNABLE

  • 当调用start()方法后变为运行状态

JavaAPI层面的RUNNABLE状态涵盖了 【操作系统】层面的**可运行状态运行状态阻塞状态**

由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行的

启动某个线程,那么该线程状态就被更改为了:RUNNABLE

BLOCKED、WAITING、TIMED_WAITING

  • 这三种阻塞状态,都属于JavaAPI层面的【阻塞状态】的细分

BLOCKED:线程是同步状态,并且在休眠等待中

WAITING:调用了join()方法,进行了等待

TIMED_WAITING:普遍就是调用了sleep()方法,进行了休眠操作

TERMINATED

  • 当线程代码运行结束

4.12 泡茶例题:

这里就是创建出两个线程,并行处理各自的事情,最后泡上一杯好茶

package com.renex.c2;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
 * 泡茶
 */
@Slf4j(topic = "DemoTea")
public class DemoTea {
    static Thread t1;
    static Thread t2;
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            try {
                log.info("洗水壶...");
                sleep(1);
                log.info("烧开水...");
                sleep(15);
                log.info("张三的活干完啦!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "张三");
        t2 = new Thread(() -> {
            try {
                log.info("洗茶壶...");
                sleep(1);
                log.info("洗茶杯...");
                sleep(2);
                log.info("拿茶叶...");
                sleep(1);
                log.info("李四的活干完啦!!");
                t1.join(); // 让t1等待一会
                log.info("泡茶啦!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "李四");
        t1.start();
        t2.start();
    }
}
//////////////////////////////////////////////
[李四] INFO DemoTea - 洗茶壶...
[张三] INFO DemoTea - 洗水壶...
[李四] INFO DemoTea - 洗茶杯...
[张三] INFO DemoTea - 烧开水...
[李四] INFO DemoTea - 拿茶叶...
[李四] INFO DemoTea - 李四的活干完啦!!
[张三] INFO DemoTea - 张三的活干完啦!!
[李四] INFO DemoTea - 泡茶啦!!

更改需求:

  • 情况一:让张三来进行泡茶的操作,需要等待李四将茶叶拿过来再进行泡茶操作
  • 情况二:张三将茶壶交给李四来泡茶
  • 情况一:
package com.renex.c2;
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
 * 泡茶
 */
@Slf4j(topic = "DemoTea")
public class DemoTea {
    static Thread t1;
    static Thread t2;
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            try {
                log.info("洗水壶...");
                sleep(1);
                log.info("水壶洗好了...");
                t2.join(); // 等待t2线程将茶叶拿过来
                log.info("接过李四的茶叶...");
                log.info("烧开水...");
                sleep(15);
                log.info("水已经烧开了!");
                log.info("张三泡茶啦!!");
                log.info("=================张三的活干完啦!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "张三");
        t2 = new Thread(() -> {
            try {
                log.info("洗茶壶...");
                sleep(1);
                log.info("洗茶杯...");
                sleep(2);
                log.info("拿茶叶送给张三...");
                sleep(1);
                log.info("===============李四的活干完啦!!");
            } catch (InterruptedException e) {
                t2.interrupt();
                e.printStackTrace();
            }
        }, "李四");
        t1.start();
        t2.start();
    }
}
////////////////////////////////////////////////////////////
[张三] INFO DemoTea - 洗水壶...
[李四] INFO DemoTea - 洗茶壶...
[张三] INFO DemoTea - 水壶洗好了...
[李四] INFO DemoTea - 洗茶杯...
[李四] INFO DemoTea - 拿茶叶送给张三...
[李四] INFO DemoTea - ===============李四的活干完啦!!
[张三] INFO DemoTea - 接过李四的茶叶...
[张三] INFO DemoTea - 烧开水...
[张三] INFO DemoTea - 水已经烧开了!
[张三] INFO DemoTea - 张三泡茶啦!!
[张三] INFO DemoTea - =================张三的活干完啦!!
  • 情况二,就交给你们来做吧!

5. 💕👉 其他好文推荐

目录
相关文章
|
1月前
|
缓存 安全 Java
JUC系列之《CountDownLatch:同步多线程的精准发令枪 》
CountDownLatch是Java并发编程中用于线程协调的同步工具,通过计数器实现等待机制。主线程等待多个工作线程完成任务后再继续执行,适用于资源初始化、高并发模拟等场景,具有高效、灵活、线程安全的特点,是JUC包中实用的核心组件之一。
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
128 2
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
136 1
|
1月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
122 1
|
1月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
138 1
|
7月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
280 67
|
6月前
|
存储 缓存 安全
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
|
5月前
|
调度 开发工具 Android开发
【HarmonyOS Next】鸿蒙应用进程和线程详解
进程的定义: 进程是系统进行资源分配的基本单位,是操作系统结构的基础。 在鸿蒙系统中,一个应用下会有三类进程:
189 0
|
8月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
8月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
559 5