JAVA并发编程艺术(二)—— Java并发机制底层实现
本系列文章只做个人读书笔记
首先明确:Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
1、volatile的分析:
volatile如何保证内存可见性:
volatile修饰的共享变量,转成汇编代码会多出一个Lock前缀的指令,Lock前缀的指令在多核处理器下会引发两件事情:
1)将当前处理器缓存行的数据写回到系统中
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
那么,问题来了,他是怎么使用其他CPU里缓存该内存地址的数据无效的呢,这就要涉及到缓存一致性,即(MESI),什么是MESI, MESI是缓存的数据的四种状态,M-Modified、E-Exclusive、S-Shared、I-Invalid,了解更多 MESI详解传送门
总结一点,其实这些状态的变更,是各CPU通过在总线上嗅探来实现的。
在看到此章节时,有个小小的疑问,请大神些解惑:
文中提到Loug lea大神在JDK7的并发包里新增一个队列集合类LinkedTransferQueue,在使用volatile变量时,用追加字节的方式来优化出队和入队的性能,这样可以把头、尾节点读入不同的缓存行,这样操作,出队,入队互不影响,这点可理解,有一点说到在队列头、尾节点都不中64字节(64位架构下,缓存行的最大字节数)时,会将它们都读到一个缓存行,我的疑问是:
头、尾节点的总字节数如果超过64字节,这个时候应该是怎么读到缓存行呢?
2、synchronized的实现原理
synchronized实现同步的基础:java中第一个对象都可以作锁(怎么知道这个对象被被的线程持有呢-这个涉及到对象头的MarkWord接下来会介绍)。
synchronized可表现为以下三种形式
1、修饰普通同步方法 锁的是当前实例对象
2、修饰静态同步方式 锁的是当前类对象
3、修饰代码块 锁的是synchronized括号里的对象
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
对象头
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下
64位架构下其存储结构:
锁的种类:
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
锁的优缺点:
3、原子操作
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”
处理器如何实现原子操作
32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性
情况下处理器不会使用缓存锁定
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
4、Java如何实现原子操作
1)使用循环CAS实现原子操作
CAS三大问题:
a) ABS问题 解决方案加版本号
b) 循环时间长开销大
c) 只能保证一个共享变量的原子操作 ,如果是多个,可以采用合并成一个共享变量来操作。
2)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。