理解多线程和线程切换

多线程同时执行可以充分利用 CPU 的多核多线程功能来显著提高应用程序的性能,在多核环境中表现的更加明显(目前市面主流 CPU都是几乎都是多核多线程),即使单核 CPU 也支持多线程。

线程是操作系统调度的最小单元,CPU 通过给每个线程分配 CPU 时间片来实现多线程执行,时间片非常短(一般几十毫秒),通过不停切换线程来执行,给人的感觉是同时执行的。

线程

现代操作系统调度的最小单元是线程,一个进程里面可以创建多个线程,每个线程都有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。

Java 原生就支持多线程,Java 程序天生就是多线程程序,一个 main() 方法会运行多个线程,也会和多个其他线程同时运行。如下示例:

1
2
3
4
5
6
7
8
9
10
public class MultiThread {

public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
}
}
}

输出如下:

1
2
3
4
5
6
7
8
[8]JDWP Command Reader
[7]JDWP Event Helper Thread
[6]JDWP Transport Listener: dt_socket
[5]Attach Listener
[4]Signal Dispatcher
[3]Finalizer
[2]Reference Handler
[1]main

线程上下文

线程上下文切换

CPU 通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

所以任务从保存到再加载的过程就是一次上下文切换。

不合理的创建过多线程或对线程管理不当会造成线程并发问题,或过多的线程上下文切换同样会带来的性能损耗,从而达不到提高应用性能的目的。

可以使用 Lmbench3 测量上下文切换的时长,使用 vmstat 测量上下文切换的次数。

减少上下文切换

减少上下文切换的方法有 无锁并发编程,CAS算法,使用最少的线程 和 使用协程

  • 无锁编程:多线程竞争锁,会引起上下文切换,所以多线程处理数据时,可以预先对数据进行处理,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS算法:Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程:避免创建不必要的线程,若任务很少而创建了大量的线程来处理,这样会存在大量的线程处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维护多个任务间的切换。

使用 jstack 命令 dump 线程信息:

1
2
3
# 3346 是 java pid 进程号
jstack 3346 > /home/dump
grep java.lang.Thread.State dump | awk '{pring $2$3$4$5}' | sort | uniq -c

多线程死锁

锁是个非常有用的工具,但多线程使用不能可能会引起死锁,一旦死锁就会造成系统功能不可用。

简单死锁示例:

  1. 有 2 把锁 A 和 B。
  2. 有 2 个线程 t1 和 t2,每个线程执行有两层锁定,两个线程持有的锁都是对方等待的锁。如,t1 线程外层持 A 锁,内层等待 B 锁;t2 线程外层持 B 锁,内层等待 A 锁。这就生产了死锁。

实际的死锁场景可能更为复杂,例如 t1 拿到锁后,因为异常情况而没有释放锁(死循环);或拿到一个数据库锁,释放锁时抛出了异常,没释放掉锁。

一旦出现死锁,业务是可感知的,因为不能继承提供服务了,那么只能通过 dump 线程查看到底是哪个线程出现了问题(线程通常表现为 Blocked 阻塞状态)。

避免死锁常见方法:

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
作者

光星

发布于

2020-05-16

更新于

2022-06-17

许可协议

评论