深入浅出Java多线程

Posted 不停的奋斗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Java多线程相关的知识,希望对你有一定的参考价值。

1. 进程:

a.程序执行过程;

b.持有资源(内存、共享文件等)和线程;

 

1、 CPU缓存
相信大家都见过下面这张图或类似的图,计算机的存储层次结构像一座金字塔。越往上访问速度越快、成本更高,所以空间也越小。越往下访问速度越慢、成本越低,空间也就越大。

CPU的运算速度最快,内存的读写速度无法和其速度匹配。假如定义cpu的一次存储或访问为一个时钟周期,那么内存的一次运算通常需要几十甚至几百个始终周期。如果在CPU直接读取内存进行运算,那么CPU大部分时间都在等在内存的访问,利用率仅有几十分之一甚至几百分之一。为了解决CPU运算速度与内存读写速度不匹配的矛盾,在CPU和内存之间,引入了L1高速缓存、L2高速缓存、L3高速缓存,通过每一级缓存中所存储的数据全部都是下一级缓存中的一部分,当CPU需要数据时,就从缓存中获取,从而加快读写速度,提高CPU利用率、提升整体效率。

L1高速缓存:也叫一级缓存。一般内置在内核旁边,是与CPU结合最为紧密的CPU缓存。一次访问只需要2~4个时钟周期
L2高速缓存:也叫二级缓存。空间比L1缓存大,速度比L1缓存略慢。一次访问约需要10多个时钟周期
L3高速缓存:也叫三级缓存。部分单CPU多核心的才会有的缓存,介于多核和内存之间。存储空间已达Mb级别,一次访问约需要数十个时钟周期。
当CPU要读取一个数据时,首先从L1缓存查找,命中则返回;若未命中,再从L2缓存中查找,如果还没有则从L3缓存查找(如果有L3缓存的话)。如果还是没有,则从内存中查找,并将读取到的数据逐级放入缓存。

2、 总线锁和缓存锁
上一篇文章讲到过lock前缀:

lock前缀,会保证某个处理器对共享内存(一般是缓存行cacheline,这里记住缓存行概念,后续重点介绍)的独占使用。它将本处理器缓存写入内存,该写入操作会引起其他处理器或内核对应的缓存失效。通过独占内存、使其他处理器缓存失效,达到了“指令重排序无法越过内存屏障”的作用

总线锁 :顾名思义就是,锁住总线。通过处理器发出lock指令,总线接受到指令后,其他处理器的请求就会被阻塞,直到此处理器执行完成。这样,处理器就可以独占共享内存的使用。但是,总线锁存在较大的缺点,一旦某个处理器获取总线锁,其他处理器都只能阻塞等待,多处理器的优势就无法发挥。

于是,经过发展、优化,又产生了缓存锁。

缓存锁:不需锁定总线,只需要“锁定”被缓存的共享对象(实际为:缓存行)即可,接受到lock指令,通过缓存一致性协议,维护本处理器内部缓存和其他处理器缓存的一致性。相比总线锁,会提高cpu利用率。

但是缓存锁也不是万能,有些场景和情况依然必须通过总线锁才能完成。

这里又出现了两个新概念:缓存行和缓存一致性协议

3、 缓存行
上一小章节中提到,缓存锁会“锁定”共享对象,如果仅锁定所用对象,那么有大有小、随用随取,对于CPU来说利用率还达不到最大化。所以采用,一次获取一整块的内存数据,放入缓存。那么这一块数据,通常称为缓存行(cache line)。缓存行(cache line)是CPU缓存中可分配、操作的最小存储单元。与CPU架构有关,通常有32字节、64字节、128字节不等。目前64位架构下,64字节最为常用。

4、 缓存一致性协议(如:MESI)

MESI协议:(原文链接:https://blog.csdn.net/lpf463061655/article/details/105719924

每个处理器都有自己的高速缓存,而又共享同一主内存。当多个处理器都涉及同一块主内存区域的更改时,将导致各自的的缓存数据不一致。那同步到主内存时该以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,来保证处理器间缓存的一致性。这类协议有MSI、MESI、MOSI等。

下面重点介绍应用较为广泛的MESI协议。MESI是Modified(修改)、Exclusive(独占)、Shared(共享)、Invaild(失效)四种状态的缩写,是用来修饰缓存行的状态。在每个缓存行前额外使用2bit,来表示此四种状态。

Modified(修改):该缓存行仅出现在此cpu缓存中,缓存已被修改,和内存中不一致,等待同步至内存。
Exclusive(独占):该缓存行仅出现在此cpu缓存中,缓存和内存中保持一致。
Shared(共享):该缓存行可能出现在多个cpu缓存中,且多个cpu缓存的缓存行和内存中的数据一致。
Invalid(失效):由于其他cpu修改了缓存行,导致本cpu中的缓存行失效。
在MESI协议中,每个缓存行不仅知道自己的读写操作,而且也监听其它缓存行的读写操作。每个缓存行的状态根据本cpu和其它cpu的读写操作在4个状态间进行迁移。

当缓存行处于Modified状态时,会时刻监听其他cpu对该缓存行对应主内存地址的读取操作,一旦监听到,将本cpu的缓存行写回内存,并标记为Shared状态
当缓存行处于Exclusive状态时,会时刻监听其他cpu对该缓存行对应主内存地址的读取操作,一旦监听到,将本cpu的缓存行标记为Shared状态
当缓存行处于Shared状态时,会时刻监听其他cpu对使缓存行失效的指令(即其他cpu的写入操作),一旦监听到,将本cpu的缓存行标记为Invalid状态(其他cpu进入Modified状态)
当缓存行处于Invalid状态时,从内存中读取,否则直接从缓存读取
总结:当某个cpu修改缓存行数据时,其他的cpu通过监听机制获悉共享缓存行的数据被修改,会使其共享缓存行失效。本cpu会将修改后的缓存行写回到主内存中。此时其他的cpu如果需要此缓存行共享数据,则从主内存中重新加载,并放入缓存,以此完成了缓存一致性。

 

公平锁和非公平锁:

公平锁,就是AQS队列中排在前面的线程先获得锁,遵循先来先得的原则。非公平锁支持插队,两者之间的差别是:

1. lock()时,非公平锁每个线程先CAS,CAS成功该线程即获得锁,失败在acquire(1)。公平锁每个线程直接acquire(1)。

2. tryAcquire时,非公平锁CAS成功就算抢到了锁,公平锁要AQS队列里没有前驱节点,且CAS成功才算抢到了锁。

非公平锁避免线程上下文频繁切换,吞吐量高,但是饥饿问题。公平锁正好相反。但是权衡后还是建议优先使用非公平锁。

以上是关于深入浅出Java多线程的主要内容,如果未能解决你的问题,请参考以下文章

深入浅出Java多线程

java 多线程怎么深入?

深入浅出java多线程编程

Java多线程(五)之BlockingQueue深入分析

Java开发三年月薪才12K,深入浅出java多线程pdf

Java开发三年月薪才12K,深入浅出java多线程pdf