1、 进程与进程概念

进程

  • 对正在运行程序的一种抽象,是资源分配和独立运行的基本单位。一个正在运行的一个应用程序就是一个进程。

    线程

  • 线程是进程中的一个执行单元,执行进程中的子任务,一个进程中至少有一个线程。

  • Java 中,线程是CPU调度的最小单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器(这里感觉要学了计算机组成原理之后会更有感觉吧!)

    并发

    在单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
    并发基础 - 图1

    并行

    多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
    并发基础 - 图2

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

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

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)

  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

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

2、 Thread的常见方法

并发基础 - 图3

sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

小结

yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片

3.3.3 线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

3.3.4 join

在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行 Test10.java

  1. private static void test1() throws InterruptedException {
  2. log.debug("开始");
  3. Thread t1 = new Thread(() -> {
  4. log.debug("开始");
  5. sleep(1);
  6. log.debug("结束");
  7. r = 10;
  8. },"t1");
  9. t1.start();
  10. t1.join();
  11. log.debug("结果为:{}", r);
  12. log.debug("结束");
  13. }

并发基础 - 图4

3.3.5 interrupt 方法详解

打断 sleep,wait,join 的线程

先了解一些interrupt()方法的相关知识:博客地址
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例Test7.java

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread() {
  3. @Override
  4. public void run() {
  5. log.debug("线程任务执行");
  6. try {
  7. Thread.sleep(10000); // wait, join
  8. } catch (InterruptedException e) {
  9. //e.printStackTrace();
  10. log.debug("被打断");
  11. }
  12. }
  13. };
  14. t1.start();
  15. Thread.sleep(500);
  16. log.debug("111是否被打断?{}",t1.isInterrupted());
  17. t1.interrupt();
  18. log.debug("222是否被打断?{}",t1.isInterrupted());
  19. Thread.sleep(500);
  20. log.debug("222是否被打断?{}",t1.isInterrupted());
  21. log.debug("主线程");
  22. }


输出结果:(我下面将中断和打断两个词混用)可以看到,打断 sleep 的线程, 会清空中断状态,刚被中断完之后t1.isInterrupted()的值为true,后来变为false,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。【同时要注意如果打断被join()wait() blocked的线程也是一样会被清除,被清除(interrupt status will be cleared)的意思即中断状态设置为false,被设置( interrupt status will be set)的意思就是中断状态设置为true
17:06:11.890 [Thread-0] DEBUG com.concurrent.test.Test7 - 线程任务执行
17:06:12.387 [main] DEBUG com.concurrent.test.Test7 - 111是否被打断?false
17:06:12.390 [Thread-0] DEBUG com.concurrent.test.Test7 - 被打断
17:06:12.390 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?true
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?false
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 主线程

打断正常运行的线程

打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread(() -> {
  3. while(true) {
  4. boolean interrupted = Thread.currentThread().isInterrupted();
  5. if(interrupted) {
  6. log.debug("被打断了, 退出循环");
  7. break;
  8. }
  9. }
  10. }, "t1");
  11. t1.start();
  12. Thread.sleep(1000);
  13. log.debug("interrupt");
  14. t1.interrupt();
  15. }

终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。
如下所示:那么线程的isInterrupted()方法可以取得线程的打断标记,如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此手动设置打断标记为true;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!
并发基础 - 图5
代码实现如下:

  1. @Slf4j
  2. public class Test11 {
  3. public static void main(String[] args) throws InterruptedException {
  4. TwoParseTermination twoParseTermination = new TwoParseTermination();
  5. twoParseTermination.start();
  6. Thread.sleep(3000); // 让监控线程执行一会儿
  7. twoParseTermination.stop(); // 停止监控线程
  8. }
  9. }
  10. @Slf4j
  11. class TwoParseTermination{
  12. Thread thread ;
  13. public void start(){
  14. thread = new Thread(()->{
  15. while(true){
  16. if (Thread.currentThread().isInterrupted()){
  17. log.debug("线程结束。。正在料理后事中");
  18. break;
  19. }
  20. try {
  21. Thread.sleep(500);
  22. log.debug("正在执行监控的功能");
  23. } catch (InterruptedException e) {
  24. Thread.currentThread().interrupt();
  25. e.printStackTrace();
  26. }
  27. }
  28. });
  29. thread.start();
  30. }
  31. public void stop(){
  32. thread.interrupt();
  33. }
  34. }


3.3.6 sleep,yiled,wait,join 对比

关于join的原理和这几个方法的对比:看这里

补充:

  1. sleep,join,yield,interrupted是Thread类中的方法
  2. wait/notify是object中的方法

sleep 不释放锁、释放cpujoin 释放锁、抢占cpuyiled 不释放锁、释放cpuwait 释放锁、释放cpu

线程状态之六种状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址
根据 Thread.State 枚举,分为六种状态 Test12.java

并发基础 - 图6

  1. NEW 跟五种状态里的初始状态是一个意思
  2. RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节
    详述