Java性能 -- 线程上下文切换
线程数量
- 在并发程序中,并不是启动更多的线程就能让程序最大限度地并发执行
- 线程数量设置太小,会导致程序不能充分地利用系统资源
- 线程数量设置太大,可能带来资源的过度竞争,导致上下文切换,带来的额外的系统开销
上下文切换
1.在单处理器时期,操作系统就能处理多线程并发任务,处理器给每个线程分配CPU时间片,线程在CPU时间片内执行任务
- CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
2.时间片决定了一个线程可以连续占用处理器运行的时长
- 当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器
- 上下文切换(Context Switch):一个线程被暂停剥夺使用权,另一个线程被选中开始或者继续运行的过程
- 切出:一个线程被剥夺处理器的使用权而被暂停运行
- 切入:一个线程被选中占用处理器开始运行或者继续运行
- 切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文
3.上下文的内容
- 寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务
- 程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置
4.当CPU数量远远不止1个的情况下,操作系统将CPU轮流分配给线程任务,此时的上下文切换会变得更加频繁
- 并且存在跨CPU的上下文切换,更加昂贵
切换诱因
1.在操作系统中,上下文切换的类型可以分为进程间的上下文切换和线程间的上下文切换
2.线程状态:NEW、RUNNABLE、RUNNING、BLOCKED、DEAD
- Java线程状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
3.线程上下文切换:RUNNING -> BLOCKED -> RUNNABLE -> 被调度器选中执行
- 一个线程从RUNNING状态转为BLOCKED状态,称为一个线程的暂停
- 线程暂停被切出后,操作系统会保存相应的上下文
- 以便该线程再次进入RUNNABLE状态时能够在之前执行进度的基础上继续执行
- 一个线程从BLOCKED状态进入RUNNABLE状态,称为一个线程的唤醒
- 此时线程将获取上次保存的上下文继续执行
4.诱因:程序本身触发的自发性上下文切换、系统或虚拟机触发的非自发性上下文切换
- 自发性上下文切换
- sleep、wait、yield、join、park、synchronized、lock
- 非自发性上下文切换
- 线程被分配的时间片用完、JVM垃圾回收(STW、线程暂停)、线程执行优先级
监控切换
样例代码
public static void main(String[] args) {
new MultiThreadTesterAbstract().start();
new SerialThreadTesterAbstract().start();
// multi thread take 5401ms
// serial take 692ms
}
static abstract class AbstractTheadContextSwitchTester {
static final int COUNT = 100_000_000;
volatile int counter = 0;
void increaseCounter() {
counter++;
}
public abstract void start();
}
static class MultiThreadTesterAbstract extends AbstractTheadContextSwitchTester {
@Override
public void start() {
Stopwatch stopwatch = Stopwatch.createStarted();
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
while (counter < COUNT) {
synchronized (this) {
if (counter < COUNT) {
increaseCounter();
}
}
}
}
});
threads[i].start();
}
for (int i = 0; i < 4; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("multi thread take {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
}
}
static class SerialThreadTesterAbstract extends AbstractTheadContextSwitchTester {
@Override
public void start() {
Stopwatch stopwatch = Stopwatch.createStarted();
for (int i = 0; i < COUNT; i++) {
increaseCounter();
}
log.info("serial take {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
}
}
1.串行的执行速度比并发执行的速度要快,因为线程的上下文切换导致了额外的开销
- 使用synchronized关键字,导致了资源竞争,从而引起了上下文切换
- 即使不使用synchronized关键字,并发的执行速度也无法超越串行的执行速度,因为多线程同样存在上下文切换
2.Redis的设计很好地体现了单线程串行的优势
- 从内存中快速读取值,不用考虑IO瓶颈带来的阻塞问题
监控工具
vmstat
cs:系统的上下文切换频率
root@5d15480e8112:/# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 0 693416 33588 951508 0 0 77 154 116 253 1 1 98 0 0
pidstat
-w Report task switching activity (kernels 2.6.23 and later only). The following values may be displayed:
UID
The real user identification number of the task being monitored.
USER
The name of the real user owning the task being monitored.
PID
The identification number of the task being monitored.
cswch/s
Total number of voluntary context switches the task made per second. A voluntary context switch occurs when a task blocks because it requires a
resource that is unavailable.
nvcswch/s
Total number of non voluntary context switches the task made per second. A involuntary context switch takes place when a task executes for the
duration of its time slice and then is forced to relinquish the processor.
Command
The command name of the task.
root@5d15480e8112:/# pidstat -w -l -p 1 2 5
Linux 4.9.184-linuxkit (5d15480e8112) 09/16/2019 _x86_64_ (2 CPU)
07:28:03 UID PID cswch/s nvcswch/s Command
07:28:05 0 1 0.00 0.00 /bin/bash
07:28:07 0 1 0.00 0.00 /bin/bash
07:28:09 0 1 0.00 0.00 /bin/bash
07:28:11 0 1 0.00 0.00 /bin/bash
07:28:13 0 1 0.00 0.00 /bin/bash
Average: 0 1 0.00 0.00 /bin/bash
切换的系统开销
- 操作系统保存和恢复上下文
- 调度器进行线程调度
- 处理器高速缓存重新加载
- 可能导致整个高速缓存区被冲刷,从而带来时间开销
竞争锁优化
- 多线程对锁资源的竞争会引起上下文切换,锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销就越大
- 在多线程编程中,锁本身不是性能开销的根源,锁竞争才是性能开销的根源
- 锁优化归根到底是减少竞争
减少锁的持有时间
- 锁的持有时间越长,意味着越多的线程在等待该竞争锁释放
- 如果是synchronized同步锁资源,不仅带来了线程间的上下文切换,还有可能会带来进程间的上下文切换
- 优化方法:将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作
减少锁粒度
锁分离
- 读写锁实现了锁分离,由读锁和写锁两个锁实现,可以共享读,但只有一个写
- 读写锁在多线程读写时,读读不互斥,读写互斥,写写互斥
- 传统的独占锁在多线程读写时,读读互斥,读写互斥,写写互斥
- 在读远大于写的多线程场景中,锁分离避免了高并发读情况下的资源竞争,从而避免了上下文切换
锁分段
- 在使用锁来保证集合或者大对象的原子性时,可以将锁对象进一步分解
- Java 1.8之前的ConcurrentHashMap就是用了锁分段
非阻塞乐观锁代替竞争锁
- volatile
- volatile关键字的作用是保证可见性和有序性,volatile的读写操作不会导致上下文切换,开销较小
- 由于volatile关键字没有锁的排它性,因此不能保证操作变量的原子性
- CAS
- CAS是一个原子的if-then-act操作
- CAS是一个无锁算法实现,保障了对一个共享变量读写操作的一致性
- CAS不会导致上下文切换,Java的Atomic包就使用了CAS算法来更新数据,而不需要额外加锁
synchronized锁优化
- 在JDK 1.6中,JVM将synchronized同步锁分为偏向锁、轻量级锁、自旋锁、重量级锁
- JIT编译器在动态编译同步代码块时,也会通过锁消除、锁粗化的方式来优化synchronized同步锁
wait/notify优化
可以通过Object对象的wait、notify、notifyAll来实现线程间的通信,例如生产者-消费者模型
public class WaitNotifyTest {
public static void main(String[] args) {
Vector<Integer> pool = new Vector<>();
Producer producer = new Producer(pool, 10);
Consumer consumer = new Consumer(pool);
new Thread(producer).start();
new Thread(consumer).start();
}
}
@AllArgsConstructor
class Producer implements Runnable {
private final Vector<Integer> pool;
private Integer size;
@Override
public void run() {
for (; ; ) {
try {
produce((int) System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void produce(int i) throws InterruptedException {
while (pool.size() == size) {
synchronized (pool) {
pool.wait();
}
}
synchronized (pool) {
pool.add(i);
pool.notifyAll();
}
}
}
@AllArgsConstructor
class Consumer implements Runnable {
private final Vector<Integer> pool;
@Override
public void run() {
for (; ; ) {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void consume() throws InterruptedException {
synchronized (pool) {
while (pool.isEmpty()) {
pool.wait();
}
}
synchronized (pool) {
pool.remove(0);
pool.notifyAll();
}
}
}
1.wait/notify的使用导致了较多的上下文切换
2.消费者第一次申请到锁,却发现没有内容可消费,执行wait,这会导致线程挂起,进入阻塞状态,这是一次上下文切换
3.当生产者获得锁并执行notifyAll之后,会唤醒处于阻塞状态的消费者线程,又会发生一次上下文切换
4.被唤醒的线程在继续运行时,需要再次申请相应对象的内部锁,此时可能需要与其他新来的活跃线程竞争,导致上下文切换
5.如果多个消费者线程同时被阻塞,用notifyAll将唤醒所有阻塞线程,但此时依然没有内容可消费
- 因此过早地唤醒,也可能导致线程再次进入阻塞状态,从而引起不必要的上下文切换
6.优化方法
- 可以考虑使用notify代替notifyAll,减少上下文切换
- 生产者执行完notify/notifyAll之后,尽快释放内部锁,避免被唤醒的线程再次等待该内部锁
- 为了避免长时间等待,使用wait(long),但线程无法区分其返回是由于等待超时还是被通知线程唤醒,增加上下文切换
- 建议使用Lock+Condition代替synchronized+wait/notify/notifyAll,来实现等待通知
合理的线程池大小
- 线程池的线程数量不宜过大
- 一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换
协程:非阻塞等待
- 协程比线程更加轻量,相比于由操作系统内核管理的进程和线程,协程完全由程序本身所控制,即在用户态执行
- 协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升
减少GC频率
- GC会导致上下文切换
- 很多垃圾回收器在回收旧对象时会产生内存碎片,从而需要进行内存整理,该过程需要移动存活的对象
- 而移动存活的对象意味着这些对象的内存地址会发生改变,因此在移动对象之前需要暂停线程,完成后再唤醒线程
- 因此减少GC的频率能够有效的减少上下文切换
Java性能 -- 线程上下文切换的更多相关文章
- 手把手教你定位常见Java性能问题
概述 性能优化一向是后端服务优化的重点,但是线上性能故障问题不是经常出现,或者受限于业务产品,根本就没办法出现性能问题,包括笔者自己遇到的性能问题也不多,所以为了提前储备知识,当出现问题的时候不会手忙 ...
- Java性能分析之线程栈详解与性能分析
Java性能分析之线程栈详解 Java性能分析迈不过去的一个关键点是线程栈,新的性能班级也讲到了JVM这一块,所以本篇文章对线程栈进行基础知识普及以及如何对线程栈进行性能分析. 基本概念 线程堆栈也称 ...
- Java性能调优笔记
Java性能调优笔记 调优步骤:衡量系统现状.设定调优目标.寻找性能瓶颈.性能调优.衡量是否到达目标(如果未到达目标,需重新寻找性能瓶颈).性能调优结束. 寻找性能瓶颈 性能瓶颈的表象:资源消耗过多. ...
- 最大化 AIX 上的 Java 性能,第 5 部分: 参考资料和结论
http://www.ibm.com/developerworks/cn/aix/library/es-Javaperf/es-Javaperf5.html 最大化 AIX 上的 Java 性能,第 ...
- 最大化 AIX 上的 Java 性能,第 2 部分: 速度需求
http://www.ibm.com/developerworks/cn/aix/library/es-Javaperf/es-Javaperf2.html 最大化 AIX 上的 Java 性能,第 ...
- Java程序性能优化读书笔记(一):Java性能调优概述
程序性能的主要表现点: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:内存分配是否合理,是否过多地消耗内存或者存在内存泄漏 启动时间:程序从运行到可以正常处理业务需要花费多少时间 负载承 ...
- Java性能调优(一):调优的流程和程序性能分析
https://blog.csdn.net/Oeljeklaus/article/details/80656732 Java性能调优 随着应用的数据量不断的增加,系统的反应一般会越来越慢,这个时候我 ...
- 《Java性能优化权威指南》
<Java性能优化权威指南> 基本信息 原书名:Java performance 原出版社: Addison-Wesley Professional 作者: (美)Charlie Hunt ...
- Java性能调优概述
目录 Java性能调优概述 性能优化有风险和弊端,性能调优必须有明确的目标,不要为了调优而调优!!!盲目调优,风险远大于收益!!! 程序性能的主要表现点 执行速度:程序的反映是否迅速,响应时间是否足够 ...
随机推荐
- C++ message queue 消息队列入门
说明:当我们有多个线程以不同的速度运行并且我们想要以特定的顺序从一个线程向另一个线程发送信息时,消息队列可能会有用. 这个想法是,发送线程将消息推送到队列中,而接收线程将消息按自己的步调弹出. 只要发 ...
- E203 CSR寄存器
RiscV架构则定义了一些控制和状态寄存器(CSR),用于配置或记录一些运行的状态.CSR寄存器是处理器内核内部的寄存器,使用专有的12位地址编码空间,对一个hart,可以配置4k的CSR寄存器. 蜂 ...
- PHP代码篇(五)--如何将图片文件上传到另外一台服务上
说,我有一个需求,就是一个临时功能.由于工作开发问题,我们有一个B项目,需要有一个商品添加的功能,涉及到添加商品内容,比如商品名字,商品描述,商品库存,商品图片等.后台商品添加的接口已经写完了,但是问 ...
- Python—五大基本语句
五大基本语句 赋值语句(变量.对象.赋值运算符) 输入输出语句(input,print函数) 条件判断语句(if-elif-else语句) 循环语句(遍历循环for-in-else.条件循环while ...
- appium---uiautomator定位方法
前面总结了7种定位方法,今天在介绍一种uiautomator方法,其实appium就是基于uiautomator框架实现的,让我们一起看下uiautomator有哪些定位方法可以使用 uiautoma ...
- 检索式chatbot:
小夕从7月份开始收到第一场面试邀请,到9月初基本结束了校招(面够了面够了T_T),深深的意识到今年的对话系统/chatbot方向是真的超级火呀.从微软主打情感计算的小冰,到百度主打智能家庭(与车联网? ...
- 剑指Offer-13.调整数组顺序使奇数位于偶数前面(C++/Java)
题目: 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变. 分析: 这道题做法有很 ...
- 9.Go-反射、日志和线程休眠
9.1反射 在Go语言标准库中reflect包提供了运行时反射,程序运行过程中动态操作结构体 当变量存储结构体属性名称,想要对结构体这个属性赋值或查看时,就可以使用反射 反射还可以用作判断变量类型 整 ...
- python--小确幸
#把手机号中间四位隐藏 def change_number(number): hiding_number=number.replace(number[3:7],'*'*4) print(hiding_ ...
- Python源码:字典
一.创建增加修改 1.实现代码 #创建 stu_info = { "xiedi":28, "liuhailin":27,"daiqiao": ...