并发2️⃣Java线程①Thread API、两阶段终止模式

1、Thread

:Thread 类实现了 Runnable 接口。

1.1、创建线程(❗)

  • 通常以匿名内部类的形式创建线程,且通常以 lambda 表达式简化。
  • 建议为每个线程指定线程名
  • 有 3 种创建线程的方式。

1.1.1、Thread

创建 Thread 匿名内部类,实现 run() 方法。

Thread t1 = new Thread("t1") {
    @Override
    public void run() {
        System.out.println("hello");
    }
};

1.1.2、Runnable & Thread(❗)

策略模式:任务和线程分开创建

创建 Runnable 匿名内部类,作为 Thread 构造方法参数。

  • 任务:创建 Runnable 匿名内部类,实现 run() 方法。

  • 线程:创建 Thread 类,参数依次为 Runnable 任务,线程名。

    // 任务
    Runnable target = new Runnable() {
        @Override
        public void run() {
            System.out.println("hello");
        }
    };
    // 线程
    Thread t2 = new Thread(target, "t2");
    

lambda 简化

// 简化
target = () -> System.out.println("hello");
t2 = new Thread(target, "t2");
// 更简化
t2 = new Thread(() -> System.out.println("hello"), "t2");

run() 源码

Thread 始终执行的是自定义的 run() 逻辑。

  • target 非空:创建 Thread 时传入了 Runnable 参数,则执行 Runnable 的方法。

  • target 为空:创建 Thread 时没有传 Runnable 参数(重写 run() 方法),则执行重写后的方法。

    private Runnable target;
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    

1.1.3、FutureTask & Thread

策略模式:任务和线程分开创建

  • 任务:FutureTask

    • 实现了 RunnableFuture 接口(间接实现 Runnable 接口),可作为 Thread 的构造方法参数传入。

    • FutureTask 实例化时需传入 Callable 参数,用于处理有返回结果的情况

    • 泛型 FutureTask 指定方法返回值类型。

    • Callable 接口:相比 Runnable 的 run()call() 具有返回值。

      @FunctionalInterface
      public interface Callable<V> {
          V call() throws Exception;
      }
      
  • 线程:创建 Thread 类,参数依次为 Runnable 任务,线程名。

示例

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "hello";
    }
};
FutureTask<String> task = new FutureTask<>(callable);
Thread t3 = new Thread(task, "t3");

t3.start();
// 主线程种获取task执行结果
String result = task.get();

1.2、查看进程、线程

Java

  • jps命令:查看所有 Java 进程。
  • jstack <PID> 命令:查看指定 Java 进程的所有线程。
  • jconsole:查看指定 Java 进程中线程的运行情况(可视化界面)

Windows

  • 任务管理器
  • tasklist命令:查看所有进程
  • taskkill命令:杀进程

Linux

Linux:进程管理

  • ps -fe:查看所有进程。
  • ps -fT -p <PID>:查看指定进程的所有线程。
  • top -H -p <PID>:查看指定进程的所有线程。
  • kill:杀进程。

1.3、线程运行原理(❗)

涉及 JVM 知识

1.3.1、虚拟机栈

  • 虚拟机栈: 每个线程运行时所需的内存,由多个栈帧(Frame)组成。
  • 栈帧:对应每个方法调用时所占内存。
    • 存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 每个线程只能有一个活动栈帧,对应当前正在执行的方法。

1.3.2、上下文切换

Thread Context Switch

内核在 CPU 上对线程进行切换,即任务切换。

可能的原因

  • 回到可运行状态
    1. 线程的 CPU 时间片用完(回顾 并行,线程轮流执行)
    2. 有更高优先级的线程需要运行。
  • 进入阻塞状态
    • 垃圾回收:GC 线程会触发 STW(stop-the-world),暂停所有用户线程。
    • 线程自身调用 sleepyieldwaitjoinparksynchronizedlock 等方法。

当 Context Switch 发生时,操作系统需要保存当前线程的状态,并恢复另一个线程的状态。

状态:包括程序计数器、每个栈帧的信息等

对应 Java 的程序计数器:

  • 若当前执行的是 JVM 的方法,程序计数器保存当前执行指令的地址。
  • 若当前执行的是 native 方法,程序计数器为空。

Context Switch 频繁发生会影响性能。

1.4、守护线程

  • 通常,Java 进程会等待所有线程结束后停止
  • 守护线程:所有其它非守护线程结束时,守护进程随即停止(即使还有代码没执行完)
  • 举例
    • 垃圾回收线程
    • Tomcat 中的 Acceptor 和 Poller 线程

2、Thread API 常用方法(❗)

获取属性的方法

  • getId():获取线程 ID(ID 具有唯一性)
  • getName():获取线程名
  • setName(String):修改线程名
  • getState():获取线程状态
    • Java 规定 6 个线程状态,以枚举类的形式表示。
    • NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
  • isAlive():判断线程是否存活(线程执行完任务则死亡)
  • currentThread():static,表示当前线程。

2.1、执行:start & run

  • start():启动一个新线程(进入就绪状态)。
  • run():线程被 CPU 调度后,执行任务。

调用 start() 和 run() 的区别

  • 线程 t1:先休眠 2 秒,再打印输出语句。

  • 主线程:分别调用 start() 和 run(),方法调用后在主线程打印输出语句。

    private static final Logger logger = Logger.getLogger(Join.class);
    
    public static void foo() {
        Thread t1 = new Thread(() -> {
            try {
                LogUtils.debug("sleeping...");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LogUtils.debug("run()执行");
        }, "t1");
        // 调用两个方法之一
        // t1.start()
        // t1.run()
        LogUtils.debug("主线程方法执行");
    }
    

结果

  • start()

    • 启动线程 t1,由线程 t1 执行 run() 方法。

    • run() 方法异步调用(主线程无需等待 run() 执行结束)

      image-20220323171813915

  • run()

    • 没有启动线程 t1,主线程直接调用 run() 方法。

    • run() 方法同步调用(主线程需等待 run() 执行结束)

      image-20220323172214760

2.2、睡眠:sleep & yield

2.2.1、sleep

使当前线程从 Running 进入 Timed Waiting 状态(阻塞),不会让出时间片

  • 线程睡眠结束后,进入就绪状态。
  • 睡眠中的线程被中断时,抛出 InterruptedException 异常。

JUC 的 TimeUnit 枚举工具类

可理解为 状态模式:每个枚举相当于一个状态,执行当前状态的不同操作。

  • 定义了不同时间单位的枚举(纳秒、微秒、毫秒、秒、分钟、小时、天)

  • 定义了基于时间的操作方法:如 timedwait()、timedJoin()、sleep()。

  • 建议使用 TimeUnit 工具类,提高可读性

    // 单位毫秒
    Thread.sleep(1);
    Thread.sleep(1000);
    Thread.sleep(1000 * 60);
    Thread.sleep(1000 * 60 * 60);
    // 可读性更高
    TimeUnit.MILLISECONDS.sleep(1);
    TimeUnit.SECONDS.sleep(1);
    TimeUnit.MINUTES.sleep(1);
    TimeUnit.HOURS.sleep(1);
    

sleep() 工具类

若需频繁使用 sleep() 功能,建议封装工具类,提前捕获方法异常。

public class SleepUtils {
    public static void sleepSeconds(int n) {
        Thread thread = Thread.currentThread();
        try {
            TimeUnit.SECONDS.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sleepMillis(int n)  {
        Thread thread = Thread.currentThread();
        try {
            TimeUnit.MILLISECONDS.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2.2、yield

使当前线程从 Running 进入 Runnable (就绪),让出 CPU 时间片

  • 调用 yield() 的线程会让出 CPU 时间片,但可能再次抢到。
  • 可能没有其它线程,yield() 无效。

2.3、优先级:prioritiy

提示(hint)调度器优先调度该线程。

仅仅是一个提示,调度器可以忽略它。

  • 多线程下,优先级高的线程,获得 CPU 时间片的几率更高。

  • 范围:1 到 10 的整数,默认 5。

  • 获取getPriority()

  • 设置setPriority(int)

    private int priority;
    
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
    
    public final int getPriority() {
        return priority;
    }
    public final void setPriority(int newPriority) {
        ...
    }
    

2.4、等待:join

等待线程执行完毕(死亡)

  • join 状态的线程被中断时,抛出 InterruptedException 异常。

  • 可读性:TimeUnit 类的 timedJoin(Thread, long) 方法
    (注意:此方法的 0 表示不等待,与 Thread 的 join() 有所区别)

  • 重载方法

    • join(long):等待线程死亡,指定最大等待毫秒数,0 表示永远等待。

    • join():等待,直到线程死亡。

      public final synchronized void join(long millis) throws InterruptedException {
          ...
      }
      public final void join() throws InterruptedException {
          join(0);
      }
      

示例:没有使用 join()

以下代码打印 r = 0,而不是预期的 20。

  • 可通过让主线程 sleep() 的方式,等待 t1 线程执行结束。

  • 缺点:不确定 t1 线程的任务执行多久,无法提前确定睡眠时间。

    static int r = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            SleepUtils.sleepSeconds(2);
            r = 20;
        }, "t1");
        t1.start();
        System.out.println("r = " + r);
    }
    

示例:使用 join()

以下代码打印 r = 20

主线程等待 t1 线程死亡之后才执行。

static int r = 0;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        SleepUtils.sleepSeconds(2);
        r = 20;
    }, "t1");
    t1.start();

    try {
        TimeUnit.SECONDS.timedJoin(t1, 0);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("r = " + r);
}

2.5、中断:interrupt

Java 中断机制:设置中断标志以提示线程 t 即将被中断,由 t 自身进行中断。

(而不是直接中断)

2.5.1、中断

interrupt()

设置中断标志,提示线程即将被中断。

  • 正常:试图中断运行状态的线程(含 park),设置中断标志为 true
  • 异常:试图中断非运行状态的线程 t(sleepwaitjoin) 。
    • 结果:t 抛出 InterruptedException 异常,清除中断标记(true → false
    • 原因:非运行状态的线程不需要被中断。

测试:分别调用 t1() 和 t2(),中断线程并查看中断标志。

主线程需休眠一会,否则与线程 t 并发执行,看不到效果。

public static void t1() {
    Thread t1 = new Thread(() -> {
        while (true) {
        }
    }, "t1");
    t1.start();

    SleepUtils.sleepMillis(10);
    // 中断
    t1.interrupt();
    LogUtils.debug(t1.isInterrupted());
}

public static void t2() {
    Thread t2 = new Thread(() -> SleepUtils.sleepSeconds(10), "t2");
    t2.start();

    SleepUtils.sleepMillis(10);
    // 中断
    t2.interrupt();
    LogUtils.debug(t2.isInterrupted());
}

结果

  • 运行的 t1:正常,设置中断标志(true)

    image-20220323184427671

  • 睡眠的 t2:异常,清除中断标志(设为 false)

    image-20220323184446124

2.5.2、判断是否中断

判断当前线程是否被中断

  • isInterrupted()不清除中断标志。
  • interrupted():静态方法,会清除中断标志。

源码:二者都调用了带参的本地方法,参数为 ClearInterrupted

顾名思义,ClearInterrupted 就是清除中断标志。

public boolean isInterrupted() {
    return isInterrupted(false);
}
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

private native boolean isInterrupted(boolean ClearInterrupted);

2.5.3、InterruptedException

  • 试图中断非运行状态的线程 t(sleepwaitjoin),会抛出 InterruptedException 异常,并清除中断标记。
  • 通常,只要声明为抛出 InterruptedException 的方法,都可以被 interrupt() 中断。

2.6、过时方法

存在安全性问题,容易破坏同步代码块。

  • stop():停止线程。
  • suspend():暂停线程。
  • resume():恢复线程。

3、模式:两阶段终止

3.1、Java 中断机制(❗)

Two Phase Termination

不是直接中断某个线程,而是 “优雅” 地中断。

  1. 设置中断标志,以提示线程 t 即将被中断
  2. 线程 t 判断中断标志,得知即将被中断,处理完相关工作后终止(如释放锁)。

实现方式

  1. 利用 isInterrupted()
  2. volatile + 停止标记

3.2、isInterrupted() 实现

案例:模拟后台监控,每 1 秒打印一次输出语句(或是打印当前时间、处理业务等)

3.2.1、流程分析

循环

  1. 判断中断标志:即 isInterruptrd(),判断是否被中断。

    • true:处理后事(如保存结果、释放锁等),结束线程。
    • false:执行任务。
  2. 执行任务:执行结束后睡眠 1 秒捕获睡眠期间的异常

    • 正常:进入下一轮循环。
    • 异常:睡眠线程被中断会清除中断标志,因此需要手动重新设置中断标志,进入下一轮循环。

    image-20220323140958368

3.2.2、实现

获取当前线程,进入循环

  1. 判断中断标志:若被中断则处理后事并结束进程。

  2. 执行任务:执行结束后睡眠 1 秒,捕获睡眠期间的异常。

  3. 睡眠期间被中断:中断标志被清除,需要重新设置中断标志

    public class TwoPhaseTermination {
        private Thread thread;
    
        public void turnOn() {
            thread = new Thread(() -> {
                Thread current = Thread.currentThread();
                while (true) {
                    if (current.isInterrupted()) {
                        // 模拟:处理后事
                        LogUtils.debug("terminating...");
                        // 处理完毕后退出
                        LogUtils.debug("Exit");
                        break;
                    }
                    // 执行任务
                    LogUtils.debug("working...");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        // 设置中断标志
                        current.interrupt();
                        e.printStackTrace();
                    }
                }
            }, "t");
            thread.start();
        }
    
        public void turnOff() {
            thread.interrupt();
        }
    }
    

3.2.3、测试

① main() 测试

开启线程,5 秒后关闭线程。

  • public static void main(String[] args) {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.turnOn();
    
        SleepUtils.sleepSeconds(5);
        tpt.turnOff();
    }
    
  • image-20220323184947588

② JUnit 测试(❗)

代码和测试效果,与 main() 测试相同。

  • 但是,JUnit 测试多线程存在一个问题:system.exit()

  • 将以上代码中的 sleep(5) 去掉,方法会直接结束,线程 t1 不会按预期运行。

    image-20220323151554878

分析org.junit.runner.JUnitCore

JUnit 测试的启动者,所有的 JUnit 测试都从 JUnitCore 的主方法开始。

对 JUnitCore 主方法的大致理解:

  1. 执行了测试代码的逻辑,获得执行结果。

  2. 判断是否成功执行,执行 System.exit()

    • 作用:终止当前虚拟机。

    • 参数:0 表示正常终止,非零表示异常终止。

      public static void main(String... args) {
          Result result = (new JUnitCore()).runMain(new RealSystem(), args);
          System.exit(result.wasSuccessful() ? 0 : 1);
      }
      

结论

  • JUnit 执行完测试代码逻辑后执行 System.exit()虚拟机被终止,因此线程 t1 也被强制终止。
  • 加入了 sleep(5),主线程会休眠 5 秒,休眠结束后才会执行测试方法中接下来的逻辑。
  • 建议:不要在 JUnit 中测试多线程。

4、应用:统筹

4.1、说明

案例:泡茶,两个人负责前期工作

要求工作总耗时最短。

  • 工作:洗水壶(1min)、茶壶(1min)、茶杯(1min)、拿茶叶(1min)、烧开水(10min)。

  • 工序:洗水壶完成后才能烧开水,所有工序完成才能泡茶。

  • 思路:使用 2 个线程模拟 2 个人,使用 sleep() 模拟工作耗时。

    • 线程 t1:洗水壶、烧开水(有序)

    • 线程 t2:洗茶壶茶杯、拿茶叶(无序)

    • 线程 t1 或 t2 泡茶。

      image-20220323160019838

注意

  1. 线程 t1 开始烧水后也可以做 t2 的部分工作,甚至可以一个线程完成,此处仅探讨两个线程的简单情况。
  2. 本题有多种解法:join、wait/notify、第三者协调。

4.2、join 实现

t2 使用 join() 等待 t1 执行结束

private static void makeTea() {
    Thread t1 = new Thread(() -> {
        LogUtils.debug("洗水壶");
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("烧开水");
        SleepUtils.sleepSeconds(10);
    }, "t1");

    Thread t2 = new Thread(() -> {
        LogUtils.debug("洗茶壶茶杯、拿茶叶");
        SleepUtils.sleepSeconds(3);

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        LogUtils.debug("泡茶");

    }, "t2");

    t1.start();
    t2.start();
}

结果

image-20220323165935933

posted @ 2022-03-22 19:45  Jaywee  阅读(65)  评论(0编辑  收藏  举报

👇