一、多线程

1. 基本概念:程序、进程、线程

1.1 程序(program)

是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

1.2 进程(process)

程序的一次执行过程,系统运行程序的基本单位,因此进程是动态的;比如打开一个软件就是一次进程(所以进程有生命周期),进程是资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
image.png

1.3 线程(thread)

线程:是轻量级的进程,但是线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程。与进程不同的是,线程作为调度和执行的单位,线程之间共享进程的方法区;但是每个线程又有自己的程序计数器虚拟机栈本地方法栈

1.4 单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个事件单元内,也只能执行一个线程的任务。比如:一个收费站,虽然后很多车道,但是只有一个工作人员再收费,只有收费了才能通过,CPU比作收费人员,如果有一个人不想交钱,那么收费人员可以把这个人“挂起”(等他想交钱了,在收费)。但是因为CPU时间单元特别短,因此感觉不出来。
  • 如果是多核的话,才能更好的发挥对线程的效率。
  • 一个Java应用程序java.exe,其实至少有三个线程:main主线程,gc垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

1.5 并行与并发

  • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事情
  • 并发:一个CPU(采用时间片段)同时执行多个任务。比如:秒杀、多个人同时做一件事

2. 线程的创建

2.1 继承Thread类

创建一个线程类,继承Thread类,重写run()方法。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

  1. public class MyThread extends Thread {
  2. public void run() {
  3. // ...
  4. }
  5. }
  1. public static void main(String[] args) {
  2. MyThread mt = new MyThread();
  3. mt.start();
  4. }

start方法只是让线程进入就绪状态,等CPU的时间片分配给线程时,才会运行。 而且start只能调用一次,否则会有IllegalThreadStateException。

2.2 实现Runnable接口

  1. public class Demo {
  2. public static class MyThread implements Runnable {
  3. @Override
  4. public void run() {
  5. System.out.println("MyThread");
  6. }
  7. }
  8. public static void main(String[] args) {
  9. new Thread(new MyThread()).start();
  10. // Java 8 函数式编程,可以省略MyThread类
  11. new Thread(() -> {
  12. System.out.println("Java 8 匿名内部类");
  13. }).start();
  14. }
  15. }

Thread和Runnable比较:

  • Java有“单继承,多实现”的特点,所以Runnable接口使用起来比Thread更灵活
  • Runnable接口出现,降低了线程对象和线程任务的耦合性
  • Runnable接口出现更符合面向对象,将线程单独进行对象的封装

我们通常优先使用“实现Runnable接口”这种方式来自定义线程类

2.3 实现Callable接口

  1. // 自定义Callable
  2. class Task implements Callable<Integer>{
  3. @Override
  4. public Integer call() throws Exception {
  5. // 模拟计算需要一秒
  6. Thread.sleep(1000);
  7. return 2;
  8. }
  9. public static void main(String args[]) throws Exception {
  10. // 使用
  11. ExecutorService executor = Executors.newCachedThreadPool();
  12. Task task = new Task();
  13. Future<Integer> result = executor.submit(task);
  14. // 注意调用get方法会阻塞当前线程,直到得到结果。
  15. // 所以实际编码中建议使用可以设置超时时间的重载get方法。
  16. System.out.println(result.get());
  17. }
  18. }

Callable与Runnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

3. 常用方法

3.1 start()&run()

这两个方法的主要区别在于:start方法使线程进入就绪状态,当cpu分配到时间片后开始运行;run方法如果直接运行则会当成是main线程里的一个普通方法来运行。

3.2 sleep()&yield()

sleep方法是Thread类的,他并不会释放锁,只会阻塞线程,并释放CPU,提供其他线程运行的机会且不考虑优先级,但如果有同步锁则sleep不会释放锁即其他线程无法获得同步锁 可通过调用interrupt()方法来唤醒休眠线程,但是会抛出InterruptedException异常;

yiled方法也是Thread类的,他会让线程进入就绪状态,它会等待CPU重新获取时间片段,但是也有可能他进入就绪状态后立马又开始执行线程了,调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳;还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会

3.3 join()

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。例如:主线程创建并启动了子线程,如果子线程中药进行大量耗时运算计算某个数据值,而主线程要取得这个数据值才能运行,这时就要用到 join 方法了;

3.4 interrupt()

打断线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。当打断sleep、wait、join等方法时,打断标记会清空;正常的线程则不会清空打断标记;

3.6 interrupted()&isInterrupted()

interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态;
isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态;

3.6 两阶段终止模式

  1. public class Demo06Interrupt {
  2. public static void main(String[] args) throws InterruptedException {
  3. TwoPhaseTermination tpt = new TwoPhaseTermination();
  4. tpt.start();
  5. Thread.sleep(3500);
  6. tpt.stop();
  7. }
  8. }
  9. class TwoPhaseTermination {
  10. private Thread monitor;
  11. public void start() {
  12. monitor = new Thread(() -> {
  13. while (true) {
  14. System.out.println(Thread.currentThread().getName() + Thread.currentThread().isInterrupted());
  15. if (Thread.currentThread().isInterrupted()) {
  16. System.out.println("料理后事");
  17. break;
  18. }
  19. try {
  20. Thread.sleep(1000); //打断情况1
  21. System.out.println("执行监控记录"); //打断情况2
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. //重新设置打断标记,但是这个去掉的话,isInterrupted是false 不能推出循环
  25. Thread.currentThread().interrupt();
  26. }
  27. }
  28. });
  29. monitor.start();
  30. }
  31. public void stop() {
  32. monitor.interrupt();
  33. }
  34. }

image.png

3.7 打断park

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

但是一旦打断之后,在执行park()方法,则不会再停下来了,这时候怎么让他重新停下来呢,可以使用3.5中的interrupted()方法;

4. 守护线程

守护线程是运行在程序后台提供服务的线程,不属于程序中不可或缺的一部分;当所有非守护线程结束时,程序也终止了,守护线程也结束了;最典型的比如GC(垃圾回收器);

5. 线程的生命周期

操作系统层面
image.png

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

Java层面
image.png
image.png

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

    6. synchronized

    二、JUC(并发)