美文网首页
并发编程的挑战

并发编程的挑战

作者: 看相声也要敲代码 | 来源:发表于2020-09-19 14:39 被阅读0次

    学以致用,方得始终

    并发编程的挑战

    在学习并发编程首先需要明确一个问题就是:并发编程是让程序处理问题更快,让程序更大限度的并行执行,而不是开辟很多线程去执行任务。当然让并发编程作为使程序解决问题更加迅速的处理方案以及程序性能判断的时候,我们不得不考虑一下几点问题:

    1. 线程上下文切换
    2. 锁问题(死锁、活锁等)
    3. 系统(硬件和软件)资源限制问题
    

    首先为什么需要考虑线程上下文切换?以及什么是上下文切换的概念,什么是死锁,死锁会导致什么问题?如何根据系统资源为程序合理的分配线程数?这些都是我们需要考虑和研究的。

    线程上下文切换

    在单核处理器这种硬件支持下,一般会有时间片的概念,时间片就是一个线程可以占用CPU的时间,一般以ms为单位。关于为什么线程工作的时候会占用CPU,请脑补操作系统知识,此处不做赘述。
    当多个线程同时执行的时候,其实在单核处理器下多个线程执行并不是齐头并进,而是分时交替获取CPU资源执行任务。而一个线程时间片执行结束后,线程处于就绪态,其他等待线程就会抢占CPU处理变为运行态

    我们首先研究一下线程的声明周期,线程的声明周期主要包括6中状态,分别为新建就绪态可运行态运行态、、阻塞态消亡。那么六种状态之间以怎样的模式进行运转呢?我们可以看一下如下这张图:

    在这里插入图片描述
    1. 新建状态:代码中使用new关键字,进行new出Thread的实例的过程
    2. 可运行状态:当在代码中new出一个Thread的实例后并不会直接执行,而是有一个中间状态,也成为就绪状态。调用start()方法之后,线程需要抢占CPU资源后变为运行状态。
    3. 运行态:当就绪线程并调度获取CPU资源后,便进入运行态,run方法中定义了线程需要执行的动作。当执行yield方法或时间片执行结束后会变为就绪态。
    4. 阻塞态:当线程在运行过程中遇到某些操作时,比如sleep和wait,会变成阻塞状态。阻塞线程需要通过notify和notifyAll对线程进行唤醒,进入就绪态。并不会立马执行run方法,而是等待机会获取CPU资源,获取线程资源。
    5. 终止:当线程执行结束或者代码终止线程时,线程会被销毁,释放资源。比如:stop方法。

    当按照时间片算法进行线程轮询使用CPU的时候,需要的操作包括,保存上一个线程的运行时状态,等待下次从就绪态获取CPU后,恢复运行状态。这样的话如果频繁进行上下文切换的话,就会产生效率问题。首先我们考虑一个问题:多线程一定快速吗?看一下结果:

    求和范围 并发(ms) 顺序(ms)
    10 1ms 0ms
    10000 1ms 1ms
    10000000 16ms 16ms
    1000000000000L 4689ms 5334ms

    所以这个时候我们就需要考虑线程个数设置多少比较合适的问题:
    多线程数量的设置一般要考虑任务的内部处理逻辑,一般分为IO密集型任务和计算密集型任务,但实际场景还需要考虑CPU,IO,内存等。

    IO密集型任务主要处理IO操作,如网络通信,文件读写等,这种依赖于DMA,占用CPU资源比较少。这种情况如果没有使用多线程处理IO密集型任务,CPU就会产生资源浪费。那IO密集型任务的线程数一般等于(CPU内核数)/(1-阻塞系数),其中阻塞系数为(0.8-0.9)

    计算密集型任务即应用中处理的时算法计算等,这种任务主要通过CPU完成,因此占用CPU资源较多。一般设置线程数为CPU内核数*2。Java可以使用一下方式设置。

    Runtime.getRuntime().availableProcessors() * 2
    

    混合型任务线程数量设置个数=(计算操作占用时间+IO操作占用时间)/计算操作占用时间。比如:混合型任务即包含计算操作也包含IO操作,假设整个任务处理时间为100ms,计算操作占用时间为20ms,IO操作占用时间为80ms,也就是说CPU只有大约五分之一在被占用,其他五分之四被浪费掉,这个时候需要启动的线程数=5。
    言归正传,我们还回到多线程上下文切换,导致的性能和效率问题上来。首先我们如何确认上下文切换次数呢?
    linux环境下可以使用vmstat测量上下文切换的次数。查看CS字段既可以看出上线文切换次数

    image.png
    如上多线程切换次数大概在600~700次之间。
    image.png

    那么如何减少线程的切换次数呢?

    减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 ·无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

    CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。 ·使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

    协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

    简单的做一个实战演练:

    减少线上WAITING线程,减少上下文切换次数。首先使用java堆栈信息分析命令jstack查看某一端口对应的进行的线程的状态。然后统计线程的状态的个数:

    grep java.lang.Thread.State dump文件 | awk '{print $2$3$4$5}' | sort | uniq -c
    

    然后分析对应状态为WAITING的线程作用,并减少线程个数。

    锁问题

    死锁:多个进程由于竞争资源,而导致程序无法推进,需要外部环境破坏非安全条件。

    如何获取死锁的状态:死锁的状态为BLOCKED状态,我们可以使用jstack获取堆栈信息,然后根据堆栈信息进行分析BLOCKED状态的线程信息,判断是否产生DeadLock问题。

    如何避免死锁呢?

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

    活锁:不同的进程在相同时间做相同的动作,就像两个人过马路,双方同时给对方让路。进程在执行,但不会往下执行。

    饥饿:根据系统资源分配策略可能会导致一些进程永远得不到服务。

    软硬件资源

    软硬件资源,比如带宽,内存等对多线程产生的影响。可以使用分布式场景下处理多线程资源的问题。比如使用hadoop等。

    参考

    《Java多线程编程艺术》

    相关文章

      网友评论

          本文标题:并发编程的挑战

          本文链接:https://www.haomeiwen.com/subject/lmbeyktx.html