线程数量

  1. 在并发程序中,并不是启动更多的线程就能让程序最大限度地并发执行
  2. 线程数量设置太小,会导致程序不能充分地利用系统资源
  3. 线程数量设置太大,可能带来资源的过度竞争,导致上下文切换,带来的额外的系统开销

上下文切换

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

切换的系统开销

  1. 操作系统保存和恢复上下文
  2. 调度器进行线程调度
  3. 处理器高速缓存重新加载
  4. 可能导致整个高速缓存区被冲刷,从而带来时间开销

竞争锁优化

  1. 多线程对锁资源的竞争会引起上下文切换,锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销就越大
  • 在多线程编程中,锁本身不是性能开销的根源,锁竞争才是性能开销的根源
  1. 锁优化归根到底是减少竞争

减少锁的持有时间

  1. 锁的持有时间越长,意味着越多的线程在等待该竞争锁释放
  2. 如果是synchronized同步锁资源,不仅带来了线程间的上下文切换,还有可能会带来进程间的上下文切换
  3. 优化方法:将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作以及可能被阻塞的操作

减少锁粒度

锁分离

  1. 读写锁实现了锁分离,由读锁和写锁两个锁实现,可以共享读,但只有一个写
  • 读写锁在多线程读写时,读读不互斥,读写互斥,写写互斥
  • 传统的独占锁在多线程读写时,读读互斥,读写互斥,写写互斥
  1. 在读远大于写的多线程场景中,锁分离避免了高并发读情况下的资源竞争,从而避免了上下文切换

锁分段

  1. 在使用锁来保证集合或者大对象的原子性时,可以将锁对象进一步分解
  2. Java 1.8之前的ConcurrentHashMap就是用了锁分段

非阻塞乐观锁代替竞争锁

  1. volatile
  • volatile关键字的作用是保证可见性和有序性,volatile的读写操作不会导致上下文切换,开销较小
  • 由于volatile关键字没有锁的排它性,因此不能保证操作变量的原子性
  1. CAS
  • CAS是一个原子的if-then-act操作
  • CAS是一个无锁算法实现,保障了对一个共享变量读写操作的一致性
  • CAS不会导致上下文切换,Java的Atomic包就使用了CAS算法来更新数据,而不需要额外加锁

synchronized锁优化

  1. 在JDK 1.6中,JVM将synchronized同步锁分为偏向锁、轻量级锁、自旋锁、重量级锁
  2. 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,来实现等待通知

合理的线程池大小

  1. 线程池的线程数量不宜过大
  2. 一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换

协程:非阻塞等待

  1. 协程比线程更加轻量,相比于由操作系统内核管理的进程和线程,协程完全由程序本身所控制,即在用户态执行
  2. 协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升

减少GC频率

  1. GC会导致上下文切换
  2. 很多垃圾回收器在回收旧对象时会产生内存碎片,从而需要进行内存整理,该过程需要移动存活的对象
  • 而移动存活的对象意味着这些对象的内存地址会发生改变,因此在移动对象之前需要暂停线程,完成后再唤醒线程
  1. 因此减少GC的频率能够有效的减少上下文切换

Java性能 -- 线程上下文切换的更多相关文章

  1. 手把手教你定位常见Java性能问题

    概述 性能优化一向是后端服务优化的重点,但是线上性能故障问题不是经常出现,或者受限于业务产品,根本就没办法出现性能问题,包括笔者自己遇到的性能问题也不多,所以为了提前储备知识,当出现问题的时候不会手忙 ...

  2. Java性能分析之线程栈详解与性能分析

    Java性能分析之线程栈详解 Java性能分析迈不过去的一个关键点是线程栈,新的性能班级也讲到了JVM这一块,所以本篇文章对线程栈进行基础知识普及以及如何对线程栈进行性能分析. 基本概念 线程堆栈也称 ...

  3. Java性能调优笔记

    Java性能调优笔记 调优步骤:衡量系统现状.设定调优目标.寻找性能瓶颈.性能调优.衡量是否到达目标(如果未到达目标,需重新寻找性能瓶颈).性能调优结束. 寻找性能瓶颈 性能瓶颈的表象:资源消耗过多. ...

  4. 最大化 AIX 上的 Java 性能,第 5 部分: 参考资料和结论

    http://www.ibm.com/developerworks/cn/aix/library/es-Javaperf/es-Javaperf5.html 最大化 AIX 上的 Java 性能,第 ...

  5. 最大化 AIX 上的 Java 性能,第 2 部分: 速度需求

    http://www.ibm.com/developerworks/cn/aix/library/es-Javaperf/es-Javaperf2.html 最大化 AIX 上的 Java 性能,第 ...

  6. Java程序性能优化读书笔记(一):Java性能调优概述

    程序性能的主要表现点: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:内存分配是否合理,是否过多地消耗内存或者存在内存泄漏 启动时间:程序从运行到可以正常处理业务需要花费多少时间 负载承 ...

  7. Java性能调优(一):调优的流程和程序性能分析

     https://blog.csdn.net/Oeljeklaus/article/details/80656732 Java性能调优 随着应用的数据量不断的增加,系统的反应一般会越来越慢,这个时候我 ...

  8. 《Java性能优化权威指南》

    <Java性能优化权威指南> 基本信息 原书名:Java performance 原出版社: Addison-Wesley Professional 作者: (美)Charlie Hunt ...

  9. Java性能调优概述

    目录 Java性能调优概述 性能优化有风险和弊端,性能调优必须有明确的目标,不要为了调优而调优!!!盲目调优,风险远大于收益!!! 程序性能的主要表现点 执行速度:程序的反映是否迅速,响应时间是否足够 ...

随机推荐

  1. 如何给HTML页面的文本设置字符和单词间距

    设置字符和单词间距介绍 属性名 单位 描述 letter-spacing px 设置字符间距 word-spacing px 设置单词间距 letter-spacing设置字符间距 letter-sp ...

  2. Sublime设置格式化代码快捷键ctrl+shift+r

    1.以管理员身份运行sublime 2.首选项---按键绑定-用户,将以下代码复制即可(这里注意不要忘记在最后一行添加逗号哦) { "keys": ["ctrl+shif ...

  3. iOS UIPopoverView的使用

    UIViewController *contentViewController = [[UIViewController alloc] init]; contentViewController.vie ...

  4. NDK简介

    一.NDK简介: C/C++的动态库.Dalvik通过JNI编程方式调用C/C++代码. NDK编程提高软件性能,加密保护APK文件 ndk-build        NDK编译生成脚本 Java编译 ...

  5. 有 va_arg宏 中数组下标-1 引发的思考 - C 语言中内存模型

    va_arg宏,是头文件 stdarg.h 中定义的,获取可变参数的当前参数. #define va_arg(list, mode) ((mode*)(list+=sizeof(mode)))[-1] ...

  6. XposedInstaller 是如何安装的

    Launcher 如何通过startActivity 传送一个 intent 到 zygote 如何 接收到它并且 fork出该app的进程的? 安装框架的时候会现有各种检查,比如当前系统版本等 下载 ...

  7. CentOS 8安装体验

    这两天出来了,晚上爽一爽. 一,下载 http://ftp.sjtu.edu.cn/centos/8.0.1905/isos/x86_64/ 还是那7G左右的保险,没有minial版了,那个500m多 ...

  8. Mybatis动态SQL(五)

    if choose (when, otherwise) trim (where, set) foreach 一.if 动态SQL通常要做的事情是有条件地包含 where 子句的一部分.比如: < ...

  9. 浅谈lowbit运算

    关于lowbit运算的相关知识 本篇随笔简单讲解一下计算机中位运算的一类重要运算方式--\(lowbit\)运算. lowbit的概念 我们知道,任何一个正整数都可以被表示成一个二进制数.如: \[ ...

  10. Educational Codeforces Round 69 (Rated for Div. 2) D. Yet Another Subarray Problem 背包dp

    D. Yet Another Subarray Problem You are given an array \(a_1, a_2, \dots , a_n\) and two integers \( ...