10-14 多线程会导致的问题

2020-07-20  本文已影响0人  西西_20f6

性能问题有哪些体现,什么是性能问题?
单线程不存在线程调度,也不存在这方面的开销,也不需要用锁也不需要用并发数据结构,
多线程除了带来效率的提高,还带来了安全问题,活跃性问题,性能问题。
可能造成服务响应慢,吞吐量低,资源消耗过高等问题。

为什么多线程会带来性能问题?

一、调度:上下文切换
1,什么是上下文?

主要是指发生线程调度的时候
什么时候会发生线程调度?
当可运行的线程数超过了CPU核数,那么操作系统就要调度线程了,以便让每个线程都有机会运行,虽然最开始的操作系统只有进程没有线程,后来线程越来越多超过了CPU核数,就带来了线程上下文切换以及调度的问题。

2,什么是上下文切换?

假设当一个线程t1运行到Thread.sleep(),这个时候t1想进入阻塞状态,线程调度器就会把t1阻塞。然后再让另一个等待CPU资源的线程t2进入runnable状态,这就是一次上下文切换,从t1上下文切换到t2上下文。这种上下文切换的开销其实非常大, 有的时候甚至比线程执行的时间更长,通常而言一次上下文切换会消耗5000~10000个CPU时钟周期,大约是几微秒,这对于CPU而言已经是非常大的开销了。

3,什么是上下文?保存现场

与程序计数器相关的,一次上下文切换,主要包含以下活动:
(1)首先挂起一个线程,比如上面的t1,然后把t1线程的状态(其实就是该线程的上下文)存在内存中的某处。一个线程上下文所包含的经典内容包括:该线程当前执行到哪个指令了,位置在哪里,因为后续要切换回来继续执行,需要跳转到阻塞之前的状态,所以就需要保存这些信息。还包括一些寄存器,这些内容都是为了今后我们切换回这个线程上下文后能继续执行。
(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

4,缓存开销(受上下文切换的影响)

对于调度而言我们的开销不仅仅是上下文切换的消耗,它的缓存也会带来开销。其实对于CPU而言我们要考虑缓存失效的问题,我们知道程序有很大概率会访问之前访问过的数据,比如for循环,所以CPU为了加快运行速度,会根据不同的算法做很多预测,把不同的数据缓存到CPU中,这样下次使用的时候很快就能取出来,但是一旦执行了上下文切换,CPU即将执行不同线程的不同代码,原来的缓存就失去价值了,所以CPU就需要重新进行缓存,这导致了线程被调度之后一开始的启动速度比较慢,因为它之前的缓存大部分都失效了。所以CPU为了防止过于频繁的上下文切换带来过于大的缓存开销,通常会设置一个最小执行时间,也就是说两次上下文切换之间的时间间隔不能小于这个最小执行时间,否则其实切换上下文开销带来的损耗都大于程序本身的执行了。

5,何时会导致密集的上下文切换?抢锁,IO

抢锁,IO或者是其他原因导致了频繁的线程阻塞,会带来大量的上下文切换,有的时候我们的程序是长时间地利用CPU做计算,那么这个时候上下文切换就比较少,

二、协作:内存同步所带来的开销

1,我们的编译器,CPU都会帮我们把程序的指令进行优化,可能进行指令重排序,来提高我们的缓存利用率,或者JVM也会把我们的锁进行优化,比如它发现我们有些锁是没有必要的,会自动删除锁。如果我们为了多线程的安全,加了很多的synchronized,volatile这些同步手段(可见性),在这种情况下,同步手段会使得很多的指令优化,CPU的缓存失效(本来有缓存CPU只要去缓存中取,现在缓存失效只能去主存中同步),其实就会影响性能。

2,关于内存方面,由于JVM规定我们是有主内存以及各个CPU自己的缓存的,如果我们使用缓存可以大大提高我们执行的速度,因为不必要每次去和主内存进行同步。但是,我们使用多线程的时候经常会使用synchronized,volatile这些关键字,有时会让不同线程的缓存失效,这样也会由于主内存同步而带来开销。


image.png

计算速度:CPU>寄存器>CPU cache>内存>外存
T0,T1分别从主内存中读取数据data,拷贝一份到自己本地存储,线程t0是run在CPU上,线程会应用到CPU本地的数据(寄存器或者cache line)来计算data,那么会产生线程本地数据和主内存不一致的情况,所以在t0计算完data后要把数据写回主存。
每一次同步主内存的数据是需要开销的。synchronized,volatile由于保证了可见性,所以每次要去同步主内存?

上一篇下一篇

猜你喜欢

热点阅读