当我们测试多个线程操作a++的时候,会出现以下结果
public class CasDemo2 {
public static void main(String[] args) {
Castest castest=new Castest();
for(int i=0;i<10;i++){
Thread thread=new Thread(castest);
thread.start();
}
}
}
class Castest implements Runnable{
private int a=0;
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":"+a++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果并没有按照我们想要的输出,我们将a++这个操作其实可以细分成三个步骤:
(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中
发现6出现了三次,说明线程3,9,4获取a的时候,并不是最新的a。此时你肯定会想到volitile关键字,private volatile int a=0;
但是结果还是会出现这种情况,可以自行尝试下。我们都知道 volitile具有内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。但是volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。后续会对volitile进行详细的说明。
此时当然可以通过synchronized来保证线程安全性
class Castest implements Runnable{
private volatile int a=0;
@Override
public void run() {
try {
synchronized (this){
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":"+a++);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private 输出结果:
synchronized是一种独占锁,也叫悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。
我们可以通过jdk源码来分析cas的原理, 首先针对上面的线程安全性,是如何通过cas实现的呢,在jdk1.5之后有个Atomic包,可以通过该包下面的方法实现
public class CasDemo2 {
public static void main(String[] args) {
Castest castest=new Castest();
for(int i=0;i<10;i++){
Thread thread=new Thread(castest);
thread.start();
}
}
}
class Castest implements Runnable{
AtomicInteger count = new AtomicInteger();
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+":"+count.getAndIncrement());
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试输出结果:
我们可以看到结果,在多线程并发情况下,并没有出现重复值的情况,每个线程拿到的都是不重复的值。看到这里有些人可能会有疑惑,atomic输出并没有像synchronized那样按顺序输出,为什么说是保证线程安全性。你可能对线程安全性有个误解,
所谓的线程安全性说简单点就是保证数据的正确性,跟顺序关系不大,要想保证线程按顺序执行方法很多,比如 线程的wait方法,join方法,wait方法,加锁等等。举个生活中的例子,比如a,b,c三人去购物,某个商品的库存只有10件,不论a,b,c谁先买,库存的逻辑正确性不会变,a买了2个,那么b,c只能有8件可以买。跟谁先买后买没关系,但是一定要保证这个操作的正确性。
点击AtomicInteger的getAndIncrement方法,可以看到如下
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以看到主要两个方法,getIntVolatile与compareAndSwapInt方法,这两个方法很好的实现了线程安全性
保证原子性的策略:
1:变量都是用Volatile关键字修饰。来保证内存可见性(getIntVolatile)
2:使用CAS算法,来保证原子性。(compareAndSwapInt)
关于volatile更好的说明可以查看这边博客:java内存模型以及volatile
Cas算法源码:
public final native boolean compareAndSwapInt(
Object var1,//操作的对象a
long var2,//对象a的地址偏移量
int var4,//对象a的期望值
int var5 //对象a的实际值
);
这个方法是native,调用C++层JVM的源码。这里有JVM的实现源码下载
链接:https://pan.baidu.com/s/1wRVNciNbT7ABGTPbR8Qlqw
提取码:lekq
Unsafe:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
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
核心方法就是cmpxchg(含义:compare and exchange)
由于这个有多个系统的实现,这里只看linux_x86架构
atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
这里使用了底层汇编语言,LOCK_IF_MP命令:根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。
lock的功能:
① 保证指令的执行的原子性
带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。
② 禁止该指令与之前和之后的读和写指令重排序
在AI-32架构软件开发者手册第8中内存排序中,有说明LOCK前缀会禁止指令与之前和之后的读和写指令重排序。这相当于JMM中定义的StoreLoad内存屏障的效果。也正是因为这个内存屏障的效果,会使得线程把其写缓冲区中的所有数据刷新到内存中。注意,这里不是单单被修改的数据会被回写到主内存,而是写缓存中所有的数据都回写到主内存。
而将写缓冲区的数据回写到内存时,就会通过缓存一致性协议(如,MESI协议)和窥探技术来保证写入的数据被其他处理器的缓存可见。
而这就相当于实现了volatile的内存语义。是的,上面我们为说明的lock前缀是如何实现volatile的内存语义就是这么保证的。
cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory
cmpxchgl的详细执行过程:
首先,输入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1···%9。也就是说,输出的eax是%0,输入的exchange_value、compare_value、dest、mp分别是%1、%2、%3、%4。
因此,cmpxchgl %1,(%3)实际上表示cmpxchgl exchange_value,(dest),此处(dest)表示dest地址所存的值。需要注意的是cmpxchgl有个隐含操作数eax,其实际过程是先比较eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等则把exchange_value的值写入dest指向的地址。如果不相等则把dest地址所存的值存入eax中。
输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。
Atomic::cmpxchg这个函数最终返回值是exchange_value,也就是说,如果cmpxchgl执行时compare_value和dest指针指向内存值相等则会使得dest指针指向内存值变成exchange_value,最终eax存的compare_value赋值给了exchange_value变量,即函数最终返回的值是原先的compare_value。此时Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl执行时compare_value和(dest)不等则会把当前dest指针指向内存的值写入eax,最终输出时赋值给exchange_value变量作为返回值,导致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失败。
CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中。否则不做更新。
CAS会有如下三个方面的问题:
1.ABA问题,一个线程将内存值从A改为B,另一个线程又从B改回到A。
2.循环时间长开销大:CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。
3. 只能保证一个共享变量的原子操作(jdk的AtomicReference来保证应用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作,解决了这一问题)。
ABA问题解决方案:在变量前面添加版本号,每次变量更新的时候都将版本号加1,比如juc的原子包中的AtomicStampedReference类。
参考资料:https://www.cnblogs.com/wildwolf0/p/11455796.html
https://www.jianshu.com/p/bd68ddf91240