并发编程②Java线程


Java 线程

1、Thread

Thread 类实现了 Runnable 接口。

1.1、创建线程

通常以匿名内部类的形式创建线程,且可以用 lambda 表达式简化。

建议为每个线程指定线程名

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 匿名内部类,实现 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()

private Runnable target;

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

target 是 Runnable 类型的成员变量。

  • target 非空:创建线程时传入了 Runnable 类型参数,执行 Runnable 的方法。
  • target 为空:创建线程时没有传 Runnable 参数,而是实现了 run() 方法(实际上是重写)

结论:无论如何,Thead 都会执行我们编写的 run() 方法逻辑

1.1.3、FutureTask & Thread

FutureTask 用于处理有返回结果的情况

也将线程与任务分开创建(的体现)

  • FutureTask 类实现了 RunnableFuture 接口,间接实现了 Runnable 接口。

  • 因此,也可以作为 Thread 的构造方法参数传入。

    public class FutureTask implements RunnableFuture {
        ...
    }
    public interface RunnableFuture extends Runnable, Future {
        void run();
    }
    

Callable 接口

FutureTask 在实例化时需要传入一个 Callable 类型的参数。

Callable 与 Runnable 相似,区别在于 Callable 具有返回值。

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

示例

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

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

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

1.2、查看进程、线程

1.2.1、Java

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

1.2.2、Windows

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

1.2.3、Linux

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

1.3、线程运行原理

涉及 JVM 知识

  • :虚拟机栈、程序计数器

1.3.1、虚拟机栈

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

1.3.2、线程上下文切换

Thread Context Switch

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

可能的原因

  1. 线程的 CPU 时间片用完(回顾 ,线程轮流执行)
  2. :GC 线程会触发 STW(stop-the-world),暂停所有用户线程。
  3. 有更高优先级的线程需要运行。
  4. 线程自身调用 sleepyieldwaitjoinparksynchronizedlock 等方法。

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

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

对应 Java 的程序计数器:

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

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

1.4、守护线程

通常 Java 进程会等待所有线程结束后停止

守护线程(daemon thread):所有其它非守护线程结束时,守护进程随即停止(即使还有代码没执行完)

举例

  • 垃圾回收线程
  • Tomcat 中的 Acceptor 和 Poller 线程

2、Thread API 常用方法

2.0、获取属性

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

2.1、start & run

说明

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

示例:调用 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

sleep() 是静态本地方法,抛出 InterruptedException 异常。

作用:使当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  • 睡眠中的线程:被打断时抛出 InterruptedException 异常(之后讲解)
  • 线程睡眠结束:进入就绪状态。

JUC 的 TimeUnit 类

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

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

  • 该类内部定义了基于时间的使用方法,不同枚举对应的方法实现不同。

  • 建议使用 TimeUnit 的 sleep() 代替 Thread 的 sleep(),可读性高。

    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() 功能,建议封装工具类,提前捕获方法异常。

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

yield() 是静态本地方法

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

  • 调用 yield() 的线程可能又抢到了时间片。
  • 可能没有其它线程,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

2.4.1、说明

join() 抛出 InterruptedException 异常

  • 重载方法

    • join(long):等待线程死亡,指定最大等待毫秒数,0 表示永远等待。
    • join():等待,直到线程死亡。
    public final synchronized void join(long millis) throws InterruptedException {
        ...
    }
    public final void join() throws InterruptedException {
        join(0);
    }
    
  • join 状态的线程被打断时,抛出 InterruptedException 异常。

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

2.4.2、场景

没有 join() 的情况

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

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(),可以通过让主线程 sleep() 的方式来等待 t1 线程执行结束。

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

使用 join()

主线程等待 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)的线程
  • 异常:中断处于 sleepwaitjoin 的线程 t,会导致 t 抛出 InterruptedException 异常,并将设置好的中断标记清除

测试:分别调用 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(告知 t1 即将被打断)

    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

强调sleepwaitjoin 的线程 t 被中断会抛出 InterruptedException 异常,并清除中断标记。

通常,只要声明为抛出 InterruptedException 的方法,都可以被 interrupt 打断。

2.6、过时方法

废弃方法:存在安全性问题,容易破坏同步代码块。

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

3、两阶段终止模式

3.1、说明

Two Phase Termination(Java 中断机制)

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

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

实现方式

本文先介绍第一种方式,之后会讲解第二种。

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

3.2、isInterrupted() 实现

3.2.1、流程分析

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

流程如下:开始循环

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

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

    • 正常:开始下一轮循环
    • 异常:睡眠线程被打断会清除中断标志,需要重新设置中断标志,进入下一轮循环
  3. 判断中断标志为 true,处理后事并结束线程。

    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.3.3、main() 测试

在主方法中进行测试

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

public static void main(String[] args) {
    TwoPhaseTermination tpt = new TwoPhaseTermination();
    tpt.turnOn();

    SleepUtils.sleepSeconds(5);
    tpt.turnOff();
}

image-20220323184947588

3.3.4、JUnit 测试(!)

在 JUnit 中进行测试

效果相同

@Test
public void test() {
    TwoPhaseTermination tpt = new TwoPhaseTermination();
    tpt.turnOn();

    SleepUtils.sleepSeconds(5);
    tpt.turnOff();
}

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

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

image-20220323151554878

分析org.junit.runner.JUnitCore

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

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

大致理解

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

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

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

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

结论

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

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