在多线程操作中,可能最经常被提起的就是数据的可见性、原子性、有序性。不管是硬件方面、软件方面都在这三方面做了很足的工作,才能保证程序的正常运行。
之前发表过一篇文章聊聊缓存一致性协议 如果感兴趣的话可以去阅读一下,里面谈到了缓存一致性的实现和处理过程,读完之后可以仔细去细想一下缓存一致性协议
到底解决了什么问题。个人理解缓存一致性协议
解决了CPU层面的可见性和一致性问题,阅读到这里可以在这里停下来,仔细回想一下缓存一致性的原理,它通过监听共享总线上消息,对自己缓存中的数据修改不同的状态,来保证数据的一致性,对自己缓存中的数据失效后,下次读取会从主存中直接读取最新的数据 ,可以保证可见性,同时保证各缓存中的数据是一致的。
软件的并发编程一样,其实除了可见性、有序性,在计算机指令在执行的过程中,CPU通过不停地切换线程执行,给每个线程分配CPU时间片来实现多线程机制,一定也会存在原子性问题,在计算机层面是怎么解决原子性问题的,这就我们今天要聊的LOCK#指令,有时也被我们称为总线锁。
LOCK指令作用
在Intel® 64 and IA-32 Architectures Software Developer’s Manual 中的章节LOCK—Assert LOCK# Signal Prefix 中给出LOCK指令的详细解释
大至翻译之后的意思如下
在CPU的LOCK信号被声明之后,在此期随同执行的指令会转换成原子指令。在多处理器环境中,LOCK信号确保,在此信号被声明之后,处理器独占使用任何共享内存。
在不大多数IA-32和Inter64位处理器中,锁可能在没有LOCK#信号的时情况下发生。请参阅下面的“IA32体系结构兼容性”部分的详细内容。
LOCK前缀只能预加在以下指令前面,并且只能加在这些形式的指令前面,其中目标操作数是内存操作数:
add、adc、and、btc、btr、bts、cmpxchg、cmpxch8b,cmpxchg16b,dec,inc,neg,not,or,sbb,sub,xor,xadd
和xchg
。如果LOCK前缀用上述列表中的指令并且源操作数是内存操作数(也就是指令没有对内存进行写操作),可能会出现未定义的
操作码异常
(ud)。如果锁前缀与任何不在上面列表中的指令一起使用,也将生成未定义的操作码异常。
xchg
指令不管有没有声明LOCK前缀,总是会声明LOCK信号。锁定前缀通常与BTS指令一起使用,在共享内存环境中,以对内存地址执行
读-修改-写
操作。锁定前缀的完整性不受内存字段对齐的影响。对于任意未对齐的字段,可以观察到内存锁定。
此指令的操作在非64位模式和64位模式下是相同的。
从P6系列处理器开始,当使用 LOCK 指令访问的内存已经被处理器加载到缓存中时,LOCK# 信号通常不会断言。取而代之的是,只锁定处理器的缓存。在这里处理器的缓存一致性机制确保对内存进行的操作是原子性的。请参见“锁定操作对内部处理器缓存的影响”,在Intel®64和IA-32体系结构软件开发人员手册第3A卷第8章中,有关锁定缓存的详细信息。
大致翻译差不多如上,核心意思主要说明LOCK指令在声明之后通过锁定总线,独占共享内存,通过一种排它的思想确保当前对内存操作的只有一个线程,然后确定在这段声明期间指令执行不会被打断,来保证其原子性。
处理器如何实现原子操作
首先处理器会保证基本的内存操作的原子性,比如从内存读取或者写入一个字节是原子的,但对于读-改-写
、或者是其它复杂的内存操作是不能保证其原子性的,又比如跨总线宽度
、跨多个缓存行
和夸页表的访问
,这时候需要处理器提供总线锁
和缓存锁
两个机制来保证复杂的内存操作原子性
总线锁
LOCK#信号就是我们经常说到的总线锁,处理器使用LOCK#信号达到锁定总线,来解决原子性问题,当一个处理器往总线上输出LOCK#信号时,其它处理器的请求将被阻塞,此时该处理器此时独占共享内存。
总线锁这种做法锁定的范围太大了,导致CPU利用率急剧下降,因为使用LOCK#是把CPU和内存之间的通信锁住了,这使得锁定时期间,其它处理器不能操作其内存地址的数据 ,所以总线锁的开销比较大。
缓存锁
如果访问的内存区域已经缓存在处理器的缓存行中,P6系统和之后系列的处理器则不会声明LOCK#信号,它会对CPU的缓存中的缓存行进行锁定,在锁定期间,其它 CPU 不能同时缓存此数据,在修改之后,通过缓存一致性
协议来保证修改的原子性,这个操作被称为“缓存锁”
什么情况下使用总线锁(LOCK#)
当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,也会使用总线锁
因为从P6系列处理器开始才有缓存锁,所以对于早些处理器是不支持缓存锁定的,也会使用总线锁
有些指令自带总线锁
BTS、BTR、BTC 、XADD、CMPXCHG、ADD、OR
等,这些指令操作的内存区域就会加锁,导致其它处理器不能同时访问它。
在上面指令中的CMPXCHG
就是JAVA里面CAS
底层常用的指令,这个指令在执行的时候,会自动加总线锁保,导致其它 处理器不能同时访问,证其原子性。
LOCK#作用总结
- 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,因为锁总线的开销比较大,后来的处理器都采用锁缓存替代锁总线,在无法使用缓存锁的时候会降级使用总线锁
- lock期间的写操作会回写已修改的数据到主内存,同时通过缓存一致性协议让其它CPU相关缓存行失效
写在最后
总线锁
、缓存锁
可以保证原子性
,缓存一致性协议
可以保证可见性
,那么JAVA中的内存模型,它做了些什么?下一篇聊聊JAVA中的内存模型(JMM)聊聊JMM