Skip to content

Java 线程

项目配置

pom.xml

xml
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

logback.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration
        xmlns="http://ch.qos.logback/xml/ns/logback"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
        </encoder>
    </appender>
    <logger name="c" level="debug" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
    <root level="ERROR">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

多线程概述

进程与线程

程序:一组静态的指令集

进程:程序运行的一个实例

线程:进程中的一个执行单元

并行与并发

并发 concurrent:单个核心通过任务调度器,线程轮流使用 CPU

并行 parallel:多个核心同时运行

同步与异步

同步:需要等待返回

异步:不需要等待返回

I/O 操作不占用 CPU

创建线程

Thread 类

java
@Slf4j(topic = "c.App")
public class App {
    public static void main(String[] args) {
        Thread t = new Thread(() -> log.info("Thread t"));
        t.setName("t");
        t.start();
        log.info("Thread main");
    }
}

Runable 接口

将任务和线程分离,代码更灵活

组合优于基础

java
@Slf4j(topic = "c.App")
public class App {
    public static void main(String[] args) {
        Runnable func = () -> log.info("Thread t");
        Thread t = new Thread(func, "t");
        t.start();
        log.info("Thread main");
    }
}

Callable 接口 & FutureTask 类

java
@Slf4j(topic = "c.App")
public class App {
    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> {
            log.info("Thread t running");
            Thread.sleep(1000);
            return 114514;
        });
        new Thread(task, "t").start();
        Integer res = task.get();
        log.info("Thread main - {}", res);
    }
}

线程运行的原理

栈与栈帧

每个线程都有一个栈,栈内有多个栈帧,每个栈帧对定一个方法。

每个线程只有一个活动帧(栈顶的栈帧),对应正在执行的方法。

线程上下文切换

导致 cpu 切换运行其他线程的原因:

  1. 线程时间片用完
  2. 垃圾回收
  3. 有更高优先级进程
  4. 线程调用了 sleep、wait 等方法

线程上下文切换时需要保存进程状态,并恢复另一个线程状态。

Thread 类方法

API

方法说明
public void start()启动一个新线程,Java虚拟机调用此线程的 run 方法
public void run()线程启动后调用的方法
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
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)将此线程标记为守护线程或用户线程

start() 和 run()

start():用于启动线程,不能被多次调用。线程启动前状态为 new,启动后变成 runable

run():线程启动后执行的代码

sleep()

  1. 睡眠当前线程,将当前线程状态将 running 变成 timed_waiting(阻塞状态)
  2. 其它线程可以使用 interrupt() 方法打断正在睡眠的线程,这时 sleep() 方法会抛出 InterruptedException 异常
  3. 建议使用 TimeUnit.SECOND.sleep() 代替 Thread.sleep(),提高可读性

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

yield()

  1. 让出当前线程 CPU 使用权,将当前线程状态将 running 变成 runable(就绪状态)
  2. 具体实现依赖于操作系统的任务调度区

join()

等待调用的线程结束,可以用于线程同步

java
@Slf4j(topic = "c.App")
public class App {
    static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        log.info("before start - {}", num);
        Thread t = new Thread(() -> num = 1, "t");
        t.start();
        log.info("after start - {}", num);
        t.join();
        log.info("after join - {}", num);
    }
}
17:18:57 [main] c.App - before start - 0
17:18:57 [main] c.App - after start - 0
17:18:57 [main] c.App - after join - 1

park()

使线程进入阻塞状态,类似于断点。遇到 interrupt() 会打断 park()

java
@Slf4j(topic = "c.App")
public class App {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            log.debug("before park()");
            LockSupport.park();
            log.debug("after park()");
        };

        Thread t = new Thread(runnable, "t");
        t.start();
        TimeUnit.SECONDS.sleep(1);
        t.interrupt();
    }
}
23:45:16 [t] c.App - before park()
23:45:17 [t] c.App - after park()

interrupt()

1、打断阻塞(sleep、wait、join、park)的线程,会清除打断状态。

java
@Slf4j(topic = "c.App")
public class App {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            log.debug("sleep");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                log.debug(e.getMessage());
            }
        }, "t");
        t.start();
        // 避免主线程先打断
        TimeUnit.SECONDS.sleep(1);
        log.info("interrupt");
        t.interrupt();
        // 等待标记被清除
        TimeUnit.SECONDS.sleep(1);
        log.info("t.isInterrupted() = {}", t.isInterrupted());
    }
}
21:21:47 [t] c.App - sleep
21:21:48 [main] c.App - interrupt
21:21:48 [t] c.App - sleep interrupted
21:21:49 [main] c.App - t.isInterrupted() = false

2、打断正常运行的线程,会存在打断状态。

java
@Slf4j(topic = "c.App")
public class App {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            log.debug("while()");
            // 使用打断标记停止线程
            while (!Thread.currentThread().isInterrupted()) ;
        }, "t");
        t.start();
        // 避免主线程先打断
        TimeUnit.SECONDS.sleep(1);
        log.info("interrupt");
        t.interrupt();
        log.info("t.isInterrupted() = {}", t.isInterrupted());
    }
}
21:21:14 [t] c.App - while (true)
21:21:15 [main] c.App - interrupt
21:21:16 [main] c.App - t.isInterrupted() = true

不推荐使用的 API

  1. stop():停止线程运行
  2. suspend():挂起(暂停)线程运行
  3. resume():恢复线程运行

如果线程持有锁,那么没有机会释放锁。

两阶段终止模式

错误思路

stop() 方法会直接停止线程。如果线程持有锁,那么没有机会释放锁。

流程分析

mermaid
graph TD
w("while(true)")-->a
a("有没有被打断?")--是-->b(料理后事)
b-->c((结束循环))
a--否-->d(睡眠 2s)
d--无异常-->e(执行监控记录)
d--有异常-->i(设置打断标记)
i-->w
e-->w

代码实现

java
@Slf4j(topic = "c.App")
public class App {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            while (true) {
                Thread thread = Thread.currentThread();
                // 监控资源
                log.debug("running");

                // 判断打断标记
                if (thread.isInterrupted()) {
                    // 释放占用资源
                    log.debug("exit...");
                    break;
                }

                // 避免大量 CPU 占用
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    thread.interrupt();
                }
            }
        };


        Thread t = new Thread(runnable, "t");
        t.start();
        TimeUnit.SECONDS.sleep(3);
        t.interrupt();
    }
}

守护线程

守护线程:只要非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

垃圾回收器线程就是一个守护线程。

sql
@Slf4j(topic = "c.App")
public class App {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            while (true) {
                log.debug("test");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        Thread t = new Thread(runnable, "t");
        t.setDaemon(true);
        t.start();
        Thread.sleep(1000);
        log.debug("main stop");
    }
}
23:59:33 [t] c.App - test
23:59:33 [t] c.App - test
23:59:33 [t] c.App - test
23:59:34 [t] c.App - test
23:59:34 [t] c.App - test
23:59:34 [main] c.App - main stop

线程的状态

五种状态

从操作系统层面描述

  1. 初始状态:创建了线程对象
  2. 可运行状态(就绪状态):线程对象与操作系统线程关联,可以被调度
  3. 运行状态:线程正在被执行
  4. 阻塞状态:线程调用了阻塞 API
  5. 终止状态:线程已经执行结束

六种状态

根据 Java 中 Thread.State 枚举类划分

  1. new:线程被创建,未调用 start() 方法
  2. runnable:运行状态 + 可运行状态 + 操作系统的阻塞状态(BIO)
  3. terminated:终止状态
  4. blocked:等待 synchronized() 获取锁
  5. waiting:等待 join()
  6. timed_waiting:有时限的阻塞状态,等待 sleep()

Released under the MIT License.