本章节三问,大家带着问题来看。
1.volatile知道是什么东西,但它原理是什么?能吹牛不是!
2.synchronize同步锁原理
3.原子操作到底怎么玩的?
解答:
在多处理器开发中保证了共享变量的“可见性”,比如当一个线程修改共享变量int a=0,另外一个线程立刻能读到这个值被修改为a=1。使用恰当的话,可以避免上下文切换哦。好处大大的!
第二步:当线程B读取共享值
通过缓存一致性协议来解决这个问题
先看看CPU的一些知识点:
内存屏障:说白了就是实现对内存操作的顺序限制
原子操作:不可终端的一个或一系列操作
缓存行、缓存命中等自己可以找找资料
下面讲解volatile的实现原则:
1)Lock前缀指令会引起处理器缓存回写到内存
该指令导致在执行指令期间,在多处理器环境中,LOCK#信号一般不锁总线,而是锁缓存。毕竟锁总线开销比较大。在P6和目前处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号,相反,会锁定这块内存区域的缓存并写回内存,并使用缓存一致性机制来确保修改的原子性,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2)一个处理器的缓存回写到内存会导致其他处理器缓存无效
在MESI(缓存一致性协议)协议中,每个Cache line有4种状态,分别是
1、M(Modified)
这行数据有效,但是被修改了,和内存中的数据不一致,数据只存在于本Cache中
2、E(Exclusive)
这行数据有效,和内存中的数据一致,数据只存在于本Cache中
3、S(Shared)
这行数据有效,和内存中的数据一致,数据分布在很多Cache中
4、I(Invalid)
这行数据无效
每个Core的Cache控制器不仅知道自己的读写操作,也监听其它Cache的读写操作,假如有4个Core:
1、Core1从内存中加载了变量X,值为10,这时Core1中缓存变量X的cache line的状态是E;
2、Core2也从内存中加载了变量X,这时Core1和Core2缓存变量X的cache line状态转化成S;
3、Core3也从内存中加载了变量X,然后把X设置成了20,这时Core3中缓存变量X的cache line状态转化成M,其它Core对应的cache line变成I(无效)
不同的处理器内部细节也是不一样的,这里就不纠结了。
synchronized语句当Java源代码被编译成bytecode的时,在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flag字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象。
在jdk1.8中,对synchronized优化了。已经不是我们之前认识的性能低下的重级锁。
synchronized的具体原理推荐一个原理文章
原子操作是不可被中断的一个或一系列操作,本质就是不能被进一步分割的最小粒度、
第一个机制是通过总线锁保证原子性(性能低)。如果多个处理器同时对共享变量进行读写改(i++操作)
那么就会被多个处理器同时操作,不是原子性,举个例子,如果a=1,我们进行i++操作,期望结果是3,但是有可能是2。
原因可能是多个处理器同时从各自的缓存中读取变量a,分别进行加1操作,然后分别写入主内存中。如何保证共享变量的原子操作,那么使用总线锁,当前处理器发出Lock#信号时,其他处理器请求将被阻塞住。那么该处理器可以独占主内存。
第二个机制是通过缓存锁定来保证原子性。 在同一时刻只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。
如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。
有两种情况下处理器不会使用缓存锁定。
1:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,则处理器会调用总线锁定。
2:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
使用自旋方式CAS实现原子操作,利用了处理器提供的CMPXCHG指令实现的,思路就是循环到进行CAS操作直到成功为止。
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
该方法的实现位于unsafe.cpp
中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
实现如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::isMP(); //判断是否是多核处理器
_asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
OCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。
1.多核处理器,为cmpxchg指令添加lock前缀。
2.不是多核处理器,就省略lock前缀。单处理器会不需要lock前缀提供的内存屏障效果
CAS缺点:
1)ABA问题: 如果一个值原来是A,变成B,又变成A,检查时发现它的值没有变化。但实际发生了变化。解决思路 是使用版本号,每次变量更新的时把版本号加1,就会变成1A–2B–3C。
2)循环时间长:如果长时间不成功,会给CPU带来非常大的执行开销。JVM如果支持paus指令,那么效率会提升不少。
3)只能保证一个共性变量的原子操作:对于多个共享变量原子操作需要通过JDK提供的AtomicReference类来保证引用对象之间的原子性。
上一篇: 并发编程的开端
下一篇: Java内存模型