[多线程] - 并发编程的性能瓶颈(CPU上下文切换与资源限制)

前言

《Java多线程编程核心技术》中相关的内容已经更新的差不多了,接下来我会将《Java并发编程的艺术》中有价值的知识点做一个梳理更新,顺便分享下电子书的链接,该系列文章配合电子书一起学习效果更佳。

Java并发编程的艺术
链接:https://pan.baidu.com/s/18H60E_8KDO9uNIuWdghzcg
提取码:2d8k

一、并发编程的瓶颈

在日常开发中,并不是我们什么问题都可以通过并行来解决,很多情况下并行的效率可能并没有串行高,就比如我们写个简单的Demo:

public class ContextSwitchDemo_01 {
    
    

	public static final long MAX = 10_000;

	// 并行
	public static void concurrent() {
    
    
		try {
    
    
			// 获取系统当前时间
			long currentTimeMillis = System.currentTimeMillis();
			Thread thread = new Thread(() -> {
    
    
				int a = 0;
				for (int i = 0; i < MAX; i++) {
    
    
					a += 5;
				}
			}, "测试线程一");
			thread.start();
			int b = 0;
			for (int i = 0; i < MAX; i++) {
    
    
				b--;
			}
			thread.join();
			long currentTimeMillis2 = System.currentTimeMillis();
			System.out.println("并行方法运行共需时间:" + (currentTimeMillis2 - currentTimeMillis)+"毫秒");
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	// 串行
	public static void serial() {
    
    
		long currentTimeMillis = System.currentTimeMillis();
		int a = 0;
		for (int i = 0; i < MAX; i++) {
    
    
			a += 5;
		}
		int b = 0;
		for (int i = 0; i < MAX; i++) {
    
    
			b--;
		}
		long currentTimeMillis2 = System.currentTimeMillis();
		System.out.println("串行方法运行共需时间:" + (currentTimeMillis2 - currentTimeMillis)+"毫秒");
	}
	// 测试入口
	public static void main(String[] args) {
    
    
		concurrent();
		serial();
	}
}

我们观察下运行结果:
在这里插入图片描述
在这里两个线程做的事情都是分别处理a,b两个数据,进行的循环次数也相同,但是最终的运行时间却是串行处理小于并行处理,这是为什么呢?其实这里主要是由于两个原因:

  1. CPU的上下文切换
  2. 创建新的线程造成的时间开销(资源限制)

二、CPU的上下文简介

1. 什么是CPU的上下文

想要了解什么是CPU的上下文环境就要先了解CPU的主要构成是什么:
在这里插入图片描述
我们首先要知道,无论我们在代码中做了多么复杂的业务处理,在硬件层面上CPU只做了三件事情:

  1. 在内存或缓存中读数据
  2. 对读到的数据做运算
  3. 将数据写会到内存或缓存中

当我们多条进程和线程同时处理业务的时候,虽然在我们的感知中所有的业务是同时进行的,但是实际上CPU中的运算单元在单位时间里只能处理一条指令。为了让我们在感知上觉得同一时间CPU做了多个业务处理,就需要CPU拥有很快的运算能力和不停的在多个线程指令间切换,在很短的时间里让运算单元雨露均沾的去处理不同线程中的指令造成使用者感知上的并发。多个线程之间的切换离不开两个硬件,一个是用来存储指令的寄存器,另外就是负责记录CPU运算到哪里的程序计数器。寄存器和程序计数器加到一起就是CPU处理指令必备的上下文环境。

2. 上下文切换

操作系统根据不同的场景对于上下文切换分三种:

  1. 进程上下文切换
  2. 线程上下文切换
  3. 中断上下文切换

虽然场景不同,但是本质上的上下文切换指的就是CPU将当前任务处理的状态进行保存(上下文信息进行保存),然后加载新的任务进行处理(加载切换到新的上下文环境)。

3. 如何减少上下文切换

在Java中我们可以使用的方法主要有三种:

  1. 在并发编程时尽量避免使用锁。当多个线程竞争锁的时候会引起上下文切换,我们可以通过很多方法来避免使用锁,如对线程ID进行算法匹配,不同线程处理不同数据段的数据。
  2. CAS算法(compare and switch):CAS算法是一种针对共享变量进行比较判断的方法,通过CAS算法更新数据可以有效的优化锁的使用。
  3. 用最少量的线程来处理任务,避免创建冗余线程造成因线程抢占引起的上下文切换。
  4. 当然我们还可以使用协程,但是Java语言本身是不支持协程的实现,需要调用三方框架。

稍晚些会专门针对CAS算法写一篇博客来讲解。

三、资源限制

1. 什么是资源限制

其实资源限制很好理解的,就比如说家里的带宽运行商分配的是1m/s,这个时候我们想要下载一些爱情动作片来补充我们的知(zi)识(shi),如果我们统一时间下载一部100m的影片只需要大概100s就可以了,但是如果我们同时下载10部100m的影片可能就需要1000s才能够看到,对于着急学习的我们肯定是不能忍的。类比到操作系统上就是我们在处理任务的时候会受到客观因素(如带宽,硬盘读写速度,CPU处理速度等)的影响,当我们在处理任务的时候需要考虑这些客观因素对于我们任务完成时效的限制。

2.如果避免资源限制

理解了什么是资源限制的话我们其实解决起来就很简单。

  1. 第一点肯定就是加钱!!!
    我们可以通过解决客观因素的限制来避免资源限制的发生,就比如搭建集群,增加带宽,当客观因素的限制域增大后,资源限制的瓶颈也会在一定程度上解决
  2. 如果我们没钱呢?
    有些时候我们公司肯定不能允许我们通过烧钱来解决问题,那么这个时候我们就要考虑一下资源复用的问题,比如我们可以通过线程池来避免过多的创建线程造成的资源开销。

四、死锁

如果说上面提到的问题都是软件层面的,那么死锁的问题绝对是因为硬件层面由于人为因素造成的。我们可以先简单的实现一个死锁:

public class DeadLockDemo_01 {
    
    

	private Object lockA = new Object();
	private Object lockB = new Object();

	public void lockA() throws InterruptedException {
    
    
		System.out.println("lockA方法运行!");
		synchronized (lockA) {
    
    
			Thread.sleep(2_000);
			synchronized (lockB) {
    
    
			}
		}
		System.out.println("lockA方法结束!");
	}

	public void deadLock() throws InterruptedException {
    
    
		System.out.println("deadLock方法运行!");
		Thread.sleep(1_000);
		synchronized (lockB) {
    
    
			synchronized (lockA) {
    
    
			}
		}
		System.out.println("deadLock方法结束!");
	}

	public static void main(String[] args) throws InterruptedException {
    
    
		DeadLockDemo_01 deadLockDemo_01 = new DeadLockDemo_01();

		new Thread(() -> {
    
    
			try {
    
    
				deadLockDemo_01.lockA();
			} catch (InterruptedException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();

		new Thread(() -> {
    
    
			try {
    
    
				deadLockDemo_01.deadLock();
			} catch (InterruptedException e) {
    
    
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();

	}
}

运行结果:
在这里插入图片描述
这里由于两个方法彼此等待互相释放锁造成了死锁,这种现象在生产上是灾难级的。为了避免死锁的造成,我们要确保以下几点:

  1. 避免同个线程持有多个锁
  2. 避免同个线程在一个锁内调度多种资源
  3. 可以多使用定时锁和嗅探锁
  4. 对于数据库连接应保证加锁指令和解锁指令由同一连接发出

至此,今天的内容就都结束了,希望有收获的同学可以点个赞,加个收藏鼓励下作者继续码字。
祝好!

猜你喜欢

转载自blog.csdn.net/xiaoai1994/article/details/111215620