预备知识

希望你不是一个初学者

线程安全问题,需要你接触过 Java Web 开发、Jdbc 开发、Web 服务器、分布式框架时才会遇到

基于 JDK 8,最好对函数式编程、lambda 有一定了解

采用了 slf4j 打印日志,这是好的实践

采用了 lombok 简化 java bean 编写

给每个线程好名字,这也是一项好的实践

进程与线程

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存,在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来记载指令,管理内存,管理 IO 的。

当一个程序运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程。

进程就可以视为程序的一个实例,大部分程序可以同时运行多个实例(列如 记事本,画图),也有的程序只能启动一个实例进程(网易云音乐)

线程

一个进程之内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。

Java中,线程作为最小调度单位,进程作为资源分配的最小单位,在windows中进程是不活动的,只是作为线程的容器。

对比

  • 进程基本上是相互独立的,而线程存于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程的通信比较复杂

    • 同一台计算机的进程通信称之为IPC(Inter process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,列入HTTP
  • 线程通信相对简单,因为他们共享进程内的内存,一个列子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本比一般要比进程上下文切换低

并行与并发

单核 CPU 下,线程实际上还是串行执行的,操作系统中有一个叫任务调度器,将 CPU 的时间片(windows下时间最小为15毫秒)分给不同的线程使用,只是由于 CPU 在线程间 (时间很短) 的切换非常快,人类感觉是同时运行的,总结一句话是微观是串行,宏观是并行

一般会将这种线程轮流使用 CPU 的做法叫做并发 concurrent

多核 cpu 下 每个核心 core 都可以调度运行线程,这时候线程是可以并行的

引用 Rob Pike 的一段描述:

并发 (current) 是同一时间应对 (dealing with) 多件事情的能力

并行 (parallel) 是同一时间动手做 (doing) 多件事情的能力

同步与异步

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

多线程可以让方法执行变为异步的(即不要巴巴干等着)

比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停...

又比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程

tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程。

ui 程序中,开线程进行其他操作,也可以避免阻塞 ui 线程

充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。

计算 1 花费 10 ms

计算 2 花费 11 ms

计算 3 花费 9 ms

汇总需要 1 ms

如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms

如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms

但是注意:后者需要在多核 cpu 才能提高效率,单核仍然时是轮流执行

总结:

  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活。
  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的,有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
  3. 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  4. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

查看进程线程

Windows

任务管理器可以查看进程和线程数,也可以用来杀死进程

tasklist 查看进程

taskkill杀死进程

Linux

ps -fe 查看所有线程

ps -fT -p <PID> 查看某个进程 (PID) 的所有线程

kill 杀死线程

top 大写H切换是否线程线程

top -H -p <PID> 查看某个进程 (PID) 的所有线程

Java

jps 查看所有Java线程

jstack <PID> 查看某个Java进程 (PID) 的所有线程状态

jconsole 来查看某个Java进程中线程的运行情况(图形界面)

jconsole 也可以用来远程监控,需要在远程机配置

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=`是否安全连接|fales\true` -
Dcom.sun.management.jmxremote.authenticate=`fales\true`

Java线程

使用 Thread

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
  @Override
  // run 方法内实现了要执行的任务
  public void run() {
    log.debug("hello");
  }
};
t1.start();

Lambda(推荐)

new Thread(() -> {
  log.debug("--running");
}, "your thread name").start();

使用 Runnable

需要搭配 Thread 一起食用

把【线程】和【任务】(要执行的代码)分开

Thread 代表线程

Runnable 可运行的任务(线程要执行的代码)

// 创建任务对象
Runnable task2 = new Runnable() {
  @Override
  public void run() {
    log.debug("hello");
  }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

同理可以使用 Lambda

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

Thread Runnable

上面提到的方法1:是把线程和任务合并在了一起,方法2:是把线程和任务分开了

用 Runnable 更容易与线程池等高级 API 配合

用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

Runnable.run() 方法归根结底就是传到了 Thread.run() 中。

使用 FutureTask

同样需要配合 Thread,FutureTask 接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
  log.debug("hello");
  return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result)

线程运行原理

栈与栈帧

Java Virtual Machine Stacks (Java虚拟机中栈)

我们都知道JVM由堆、栈、方法区组成,其中栈内存是给谁用的,其实就是线程,每个线程启动后,虚拟机就会为它分配一块栈内存

  • 每个栈由多个 栈帧(Frame) 组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一次活动栈帧,对应着当前正在执行的那个方法

在下图中,我们看到 main 线程栈中,最终拥有3个栈帧,原因是一共调用到了3个方法,分别是 main 方法,method1,method2,当 method2 活动结束后就会被回收,也就是最后代码执行完栈帧会为空。

IDEA 打断点时,选择 Thread ,可以看到两个线程的栈内存是相互独立的,互不干扰。

线程上下文切换

因为一些原因导致 cpu 不再执行当前线程,转而执行另一个线程的代码,就会发生线程上下文切换(Thread Context Switch)

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级线程需要运行
  • 线程自己调用了sleep,yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统,保持当前线程状态,并恢复另一个线程的状态, Java 中对应的概念就是程序计数器(Program Counter Register),他的作用时记住下一条 jvm 指令的执行地址, 是线程私有的,状态包括程序计数器 虚拟机栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等。

注意:Context Switch 频繁发生会影响性能

常用方法

方法名static功能说明注意
start() 启动一个新线程,在新的线程运行run方法中的代码start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU时间片还没有分给他)。每个线程对象的start方法只能调用一次,多次调用会出现 IllegalThreadStateExceoption
run() 新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join() 等待线程结束
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程长整型idid唯一
getName() 获取线程名字
setName(String) 设置线程名字
getPriority() 获取线程优先级1-10
setPriority(int) 修改线程优先级java 中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW,RUNNABLE, BLOCKED,WAITING, TIMED_WAITING,TERMINATED
isInterrupted() 判断是否被打断不会清除打断标记
isAlive() 线程是否存货(还没有运行完毕)
interrupt() 打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前线程休眠n毫秒,休眠时让出cpu给其他线程
yield()static提示线程调度器,让出当前线程堆CPU的调用主要是为了测试和调试

run 与 start

直接调用 run 是在主线程中执行了 run,没有启动新的线程

使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

sleep

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yieId

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。

Runnable task1 = () -> {
  int count = 0;
  for (;;) {
    System.out.println("---->1 " + count++);
  }
};
Runnable task2 = () -> {
  int count = 0;
  for (;;) {
    System.out.println(" ---->2 " + count++);
  }
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

join

x.join() 等待 x 线程执行完后再继续。意味着同步操作。

下面的案例中,打印出的 r 值是什么呢?

static int r = 0;
public static void main(String[] args) throws InterruptedException {
  test1();
}
private static void test1() throws InterruptedException {
  log.debug("开始");
  Thread t1 = new Thread(() -> {
    log.debug("开始");
    sleep(1);
    log.debug("结束");
    r = 10;
  });
  t1.start();
  log.debug("结果为:{}", r);
  log.debug("结束");
}

因为主线程和线程 t1 是并行执行的,t1线程需要1秒之后才能算出 r = 10

而主线程一开始就要打印 r 的结果,所以只能打印出 r = 0

解决方法:可以用 join(),加在 t1.start() 之后即可。

另外:join() 里面可以传毫秒,超过该时间就不等了,不超过的话等待时间就取决于线程本身了。

应用

需要等待结果返回,才能继续运行就是同步

不需要等待结果返回,就能继续运行就是异步

看下面的代码,分别两个线程

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
  Thread t1 = new Thread(() -> {
    try {
      TimeUnit.SECONDS.sleep(1); //休眠1秒
      r1 = 10;
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  },"t1");

  Thread t2 = new Thread(() -> {
    try {
      TimeUnit.SECONDS.sleep(2); //休眠两秒
      r1 = 20;
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  },"t2");

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

  long start = System.currentTimeMillis();
  log.debug("join begin");
  t1.join();//等待t1线程结束
  
  log.debug("t1 join end");
  t2.join();

  log.debug("t2 join end");
  long end = System.currentTimeMillis();
  log.debug("r1:{},r2:{},cost:{}",r1,r2,end-start);
}

第一个 join 等待 t1 时,t2 并没有停止,而在运行

第二个 join 1s 后执行到此 t2 也运行了1s,因此也只需要等待 1s

interrupt

打断 sleep、wait 、join 的线程,这几个方法都会让线程进入阻塞状态

打断 sleep 的线程,会清空打断状态,以 sleep 为例子

private static void test1() throws InnterruptedException {
  Thread t1 = new Thread(() ->{
    TimeUnit.SENCONDS.sleep(1);
  },"t1");
  t1.start();
  TimeUnit.SENCONDS.sleep(0.5);
  t1.interrupt();
  log.debug("打断状态:{}",t1.isInterrupted())
}

打断正常的线程

public static void main(String[] args) throws InterruptedException {
  Thread t1 = new Thread(() -> {
    while(true) {
      boolean interrupted = Thread.currentThread().isInterrupted();
      if (interrupted) {
        log.debug("被打断,退出循环");
        break;
      }
    }
  },"t1");
  t1.start();
  Thread.sleep(1000);
  log.debug("interrupt");
  t1.interrupt();
}

过时不推荐

不推荐使用的方法,已过时且会造成锁的释放问题。

方法名功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

主线程和守护线程

默认情况下,Java线程需要等待所有线程都运行结束,才会停止,有一种特殊线程叫守护线程,只要其他非守护线程运行结束,即使守护线程的代码没有执行完,也会强制结束。

public static void main(String[] args) {
  Thread a = new Thread(() -> {
    while (true) {
      if (Thread.currentThread().isInterrupted()) {

      }
    }
  },"A");
  a.setDaemon(true);
  a.start();
  log.debug("end");
}

垃圾回收线程就是一种守护线程

Tomcat 中的 Acceptor 和 Poller 线程就是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完请求。

总结

线程创建

线程重要 api 如 start run sleep join interrupt

线程状态

应用方面

  • 异步调用:主线程执行期间,其他线程异步执行耗时操作
  • 提高效率:并行计算,缩短运算时间
  • 同步等待:join
  • 统筹规划: 合理使用他线程 得到最优效果

原理方面

  • 线程运行流程:栈、栈帧、上下文切换、程序计数器
  • Thread 两种创建方式的源码

模式方面

  • 终止模式之两阶段终止

线程状态

五种状态

这是从操作系统层面来描述的。

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

【运行状态】指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

【阻塞状态】

  • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
  • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
  • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

根据 Java API 层面描述的,根据 Thread.State 枚举 分为六种状态。

NEW 线程刚被创建,但是还没有调用 start() 方法

RUNNABLE 当调用了 start() 方法 注意,Java API 层面的 RUNNABLE 状态覆盖操作系统层面的可以运行状态、运行状态和阻塞状态, (由于BIO 导致线程阻塞,在 Java 里无法区分,任然认为可以运行。

BOLCKEDWAITINGTIMED_WAITING 都是 Java API 层面堆【阻塞状态】的细分,之后会慢慢解释。

TERMINATED 当线程代码运行结束。

新建 、可运行(就绪)、阻塞、等待、死亡、定时等待


Last modification:March 31, 2021
如果觉得我的文章对你有用,请随意赞赏