引入
-
在Java中实现并发用的最多的就是
synchronized
关键字了,自从jdk1.6对synchronized
进行重大优化后,其广为人诟病的性能问题也得到了改善,与ReentrankLock
相比性能方面相差无几 -
性能的改善得益于偏向锁、轻量级锁的引入,它们具体的实现方式可参考《Java并发编程的艺术》和《深入理解Java虚拟机》这两本书。偏向锁、轻量级锁和重量级锁不同的地方在于不是通过信号量机制(强制阻塞)而是通过自旋CAS实现互斥访问的,避免了强制阻塞时用户态与核心态之间切换带来的开销(系统调用),这里的开销主要是保存用户态的上下文信息。
自旋CAS
-
CAS操作是一个原子操作,所谓原子操作就是指在执行期间不会被其他线程打断,要么执行完毕,要么不执行
-
CAS操作有三个操作数V(内存地址),A(旧的预期值),B(准备设置的新值)。指令执行时先看V指向的内存中存储的值是否和A相同,如果相同才会更新为B,否则什么也不做。
-
自旋是指当对象或同步块已经被其他线程锁定时,竞争线程空转等待占用线程执行完毕的情形。注意此时竞争线程并没有阻塞,而是原地空转,执行一个无限循环判断对象是否已解锁,所以不存在用户态到核心态的转换,因而同步效率较高(但会占用CPU时间)。
从问题出发理解原子性
-
我一直有个疑惑,CAS的原子性和使用CAS加锁保证线程安全有什么关系?假设有多个线程同时在对同一块内存进行CAS操作的话,那不就有可能出问题吗:两个线程T1,T2同时对同一对象执行CAS操作加锁,V存储同一块内存地址,A当然也是同样旧的预期值,那么这种情况下T1和T2都可以进行更新,那么CAS操作加锁过程就是无效的,因为CAS操作成功后线程就会进入同步块,此时就会有多个线程同时执行同步块中的代码······那这不就会使同步块线程不安全了吗。
-
后来我明白了CAS原子性和线程安全的关系,在多个线程同时CAS的情况下是不会发生多个线程CAS成功的情况的,因为计算机底层实现保证了V指向内存的互斥性和立即可见性,可以理解为CAS操作是底层保证的线程安全
-
首先说结论,一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。
-
处理器实现原子操作有两种做法
-
一是总线锁,在多CPU 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个
LOCK#
信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据 -
二是缓存锁,如果共享内存已经被缓存,那么锁总线没有意义。缓存锁核心是使用了缓存一致性协议,如
MESI
协议-
MSEI表示缓存行的四种状态
-
M(Modify)
表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致 -
E(Exclusive)
表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改 -
S(Shared)
表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致 -
I(Invalid)
表示缓存已经失效
-
-
在
MESI
协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作 -
CPU在读数据时,如果缓存行状态是I,则需要从内存中读取,并把缓存行状态置为S;如果不是I,则可以直接读取缓存中的值,但在此之前必须要等待对其他CPU的监听结果,如果其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存后再读取
-
CPU可以将状态为
M/E/S
的缓存写入内存,其中如果缓存行状态为S,则其他CPU缓存了相同数据的缓存行会无效化
-
-
-
也就是说不会有多个线程同时访问共享变量,而且共享变量更新是对所有线程可见的,所以原子操作是线程安全的。