上文结尾写到循环CAS实现原子操作
java中可以通过锁和循环CAS的方式实现原子操作
使用循环CAS实现原子操作,JVM中CAS操作正是利用了处理器提供的CMPXCHG实现的,自旋CAS实现的基本思路是循环进行CAS操作成功为止,
CAS存在ABA问题,a->b->a好像a没有变 使用版本号可解决
循环时间长开销大 JVM提供pause,使cpu不消耗过多资源, 避免退出循环时因内存顺序冲突引起CPU流水线清空.
只能保证一个共享变量的原子操作. 合并变量.
使用锁实现院子操作,除了偏向锁JVM实现锁都使用了循环CAS,进入同步块循环CAS获取锁,退出循环CAS释放锁
3.Java内存模型
(java内存模型基础,java内存模型中顺序一致性,重排序与顺序一致性模型,同步原语synchronized volatile和final的内存语义和重排序规则在处理器中的实现,java内存模型的设计原理与处理器内存模型和顺序一致性内存模型的关系)
1.Java内存模型基础
class字节码->class加载器->(
堆,方法区,() 虚拟机栈, 程序计数器,本地方法栈(线程独占)
)
(这里java内存模型的分析将在之后读的深入JVM底层的笔记中详细叙述)
首先Java的JMM(java 内存模型不是真实的物理模型,是一种抽象的模型) 它直接抽象出主内存和工作内存两个概念,每个线程启动时JVM都会为其创建一个工作内存(也可叫栈空间)用于存储线程私有的数据,工作时,将变量由主内存拷入工作内存,操作完成之后再拷回主内存,如何防止出现不同工作内存导致的不一致性结果呢,jmm用八种操作完成
lock unlock
read load read作用于主内存,将主内存的值读出主内存 load作用于工作内存 将值读入工作内存中
use asign use作用于工作内存 将工作内存中的值读出给执行引擎 assign作用于工作内存 将引擎中的值赋值给工作内存
store write store作用于工作内存中的变量,取出传递给主内存,write同作用于工作内存中的变量,写入主内存中
X86 X64 AMD64处理器仅支持对StoreLoad指令的重排序(因为他们都使用了写缓冲区)
写一个DoubleCheck变量,即使使用了锁 也只是在单线程环境下没问题,如果多线程 instance初始化与赋值任然会排序优化,可能导致读取的instance异常,因此这个instance应该加volatile 防止指令排序优化(这里周末补上对应的代码的不加volatile的测试)
JMM针对编译器制定的volatile重排序规则表
第二个操作 普通读写 volatile 读 volatile 写
第一个操作
普通读写 可重排 可 不可
volatile读 不可 不可 不可
volatile写 可 不可 不可
下面分析普通变量读写与volatile读写间重排序的两个不可
读一个volatile变量时,JMM将该线程对应的本地内存设置为无效.接下来从主内存中读取共享变量(所有共享变量 不仅仅是volatile变量)
同样写也是
所以 第一个操作是volatile读时 普通读写 volatile读写都不可与之重排序
第二个是写操作时, 之前的任何操作都不可以换
volatile写与volatile读不可交换
这里很多博客将的都不够清楚,这里我看到一篇https://blog.youkuaiyun.com/Unknownfuture/article/details/105023355
如果这里忘了或者不懂可以去看看
重排序包括 编译器重排序 处理器重排序
要理解volatile特性,可以将对volatile变量的单个读写当做使用同一个锁对这些单个读/写操作做同步
(这里有一个test可以写 周末补上)
下面分析volatile变量的JMM层实现,
保守策略下在每个Volatile写操作的前面加一个StoreStore 后面加一个StoreLoad
在每个Volatile读操作后面插入一个LoadLoad LoadStore屏障
首先分析Volatile写前加StoreStore屏障 可以保证普通写 Volatile写 不会与之交换顺序
之后加StoreLoad 可以保证 volatile读 写 不会与之交换顺序
然后Volatile读后加LoadLoad 可以保证后面的V读普通读不交换
加LoadStore保证后面的普通写 V写不交换
(focus on!)这里建议还是不够清楚 需要想办法补充
其实 这种保守做法在很多时候多了很多额外的屏障影响效率 因此编译器很多时候会自动优化一些冗余的屏障.
这里Volatile是增强过的,作者推荐我们去看看Brian Goetz的文章《Java理论与实践:正确使用Volatile变量》(Mark 如果找到合适的资源要看)
锁的内存含义 ReentrantLock查看锁的内存含义(包含fairLock 和unfairLock)
依赖AbstractQueuedSynchronizer(Java同步器框架)
具体关系图大概是 AbstractQueuedSynchronizer
Sync
fairSync nonfailSync
ReentrantLock
这里需要自己复现一下ReentrantLock(Mark)
其中使用了CompareAndSet原子操作
分析CAS操作如何同时具有Volatile读和Volatile写的含义
编译器角度 编译器不对Volatile读和之后的内存操作重排序,不对Volatile写和写前的内存操作重排序,因此这里说明编译器不对CAS之前和之后的内存操作进行重排序
处理器角度
查看CompareAndSwapInt方法的源码中调用的C++代码,源代码中指示 程序会根据当前处理器类型决定是否为cmpxchg添加lock前缀 多处理器就加 单处理器就省略
lock前缀在Intel手册中的说明
1.部分处理器选择直接锁总线来保证别的处理器无法通过总线访问内存 另一部分使用缓存锁定保证指令执行的原子性
2.禁止该指令与前后的读写指令重排序
3.写缓冲区的数据刷新到内存
综上 实现锁的内存语义可以volatile或者利用CAS附带的Volatile含义.
final域的重排序规则
1在构造函数内对一个final域的写不能与随后把这个被构造对象的引用赋值给一个引用变量重排序
(JMM禁止编译器把final域的写重排序到构造函数外, 编译器在final域写后构造函数return之前插入store store屏障,禁止处理器把final域的写重排序的构造到构造函数之外)
2初次读一个包含final域的引用与之后初次读这个final域之间不能重排序
在final域的读之前插入loadload屏障
(仅仅针对处理器,大多数处理器遵守间接依赖,但少数处理器如alpha允许重排序这种存在间接依赖关系的指令重排序,这种规则是专门针对这种处理器的)
如果final域为引用类型
写final域的重排序规则对编译器和处理器增加规则:在构造函数内对一个final引用对象的成员域的写入与随后把这个被构造对象的引用赋值给引用变量不可重排序.
final引用不能从构造函数中溢出.
final在X86处理器中的实现,X86不支持对storestore重排序,且不会对存在间接依赖关系的操作重排序,因此LoadLoad也被省略,也就是x86中 final域的读写不插入任何变量
3.7JMM的设计
JMM设计要考虑两个关键因素:
1.java程序员希望内存模型越强越好,自己可以尽量不关注内存级别的操作
2.处理器,编译器设计者希望内存模型越弱越好,如果内存模型束缚越少,他们可以进行的优化就越多
这里作者又抛出一篇文章,Mark一下(《Time,Clocks and the Ordering of Events in a Distributed System》)
双重检查锁定和延迟初始化
双重检查锁定是常见的延迟初始化的方法但他是错误的用法,(其实就是讲synchronized的代码)
instance=new instance可以分解为
memory=allocate();
init(memory)
instance=memory;
2 3行之间是可能发生重排序的
java规范中intra-threadsemantics,是保证单线程内执行结果是不可以因为重排序改变的
图取自书(侵删)
这里读者引出两种解决思路
1 不允许2 3 排序
2 不允许 2 3重排序被其它线程看见
1基于 Volatile的解决方案
只要把instance修改为Volatile的就可以
2基于类初始化的解决方案
静态内部类初始化
以下类或接口将被立即初始化
1.T是一个类 一个T类型的实例被创建
2.T是一个类 且T中声明的一个静态方法被调用
3.T中声明的一个静态字段被赋值
4.T中声明的静态字段被使用且不是一个常量字段
5.T是一个顶级类,而且一个断言语句嵌套在T内部被执行
静态内部类初始化是情况4
java语言多线程环境下可能有多个线程同时执行getInstance方法,因此在java中初始化一个类或者接口时要做细致的同步处理
Java语言规范规定对于每一个类/接口 都有一个唯一的初始化锁与之对应,具体C->LC的映射由jvm自己实现,jvm在类初始化期间会获取这个初始化锁,每个线程保证至少获取一次锁保证已经被初始化过了.类初始化五阶段:
1.通过在Class对象上同步 控制类的初始化, 获取到,看到未初始化,设置为initializing.B等待获取锁
2.A执行类初始化,B得到初始化锁,看到state,释放锁,在初始化锁的condition中等待
3.线程A设置state=initialized 唤醒在condition中等待的线程
4.线程B结束类的初始化处理(???这里没明白)
5.线程C执行类的初始化处理