上线文切换
巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。

扩展知识
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
进程
(有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。
上下文
是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器
是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
程序计数器
是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
PCB-“切换桢”
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。
上下文切换的活动
切换的原因
- 当前线程的时间片耗尽
- 垃圾回收
- 有更高优先级的线程需要执行
- 线程自己调用 sleep、yield、wait、join、park、synchronized、lock 等方法
切换过程的动作
- 由操作系统保存当前线程的状态,并恢复另一个线程的状态(通过程序计数器完成)
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
引起线程上下文切换的原因
- 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
- 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
- 用户代码挂起当前任务,让出 CPU 时间;
- 硬件中断;
上线文切换次数监测
- 使用
Lmbench
可以测量上线文切换的时长 - 使用
vmstat
可以查看上下文切换的次数
减少上下文切换的方式
- ❑ 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- ❑ CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- ❑ 使用最少线程。合理使用线程池。
- ❑ 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
死锁问题
多线程互相需要对方的锁,而且都不释放就会出现死锁。
避免死锁的方式
- ❑ 避免一个线程同时获取多个锁。
- ❑ 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- ❑ 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- ❑ 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
- ❑ 避免死锁要注意加锁顺序
- ❑ 如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
死锁定位
- 可用工具jconsole工具,或者jps获取线程id在使用jstack定位
活锁问题
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
1 | package com.sunld.thread; |
饥饿问题


资源限制问题
- 资源限制:在并发编程过程中,程序的执行速度受限于物理硬件资源或软件资源,比如:节点网络、磁盘读写、CPU处理速度、数据库连接
- 由于资源组不足,在多线程并发过程中会降低程序的运行效率,可能比串行还慢(由于上线文切换)
- 解决方案:合理使用线程池、复用socket连接、多路复用
变量线程安全分析
成员变量、类变量
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
基本类型
1 | package com.sunld.thread; |
1 | public static void testSafe1(); |

引用类型–成员变量

引用类型–局部变量

常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
参考
- 《Java并发编程的艺术》