上篇文章有说过 多线程环境下 进行变量属性 自增操作时会造成线程不安全的情况,也有说到 volatile 关键字,最后也不能保证线程安全,因为多线程情况下 他不能保证原子性,不能保证写操作过程不可以被插队,最后有提到java.util.current.atomic包中的AtomicInteger类,那么它是如何实现线程安全的呢?,让我们一探究竟!
atomc包是java专门提供保证原子性的包,里边提供了基本类型的原子操作类,天生就是保证变量原子性的。
今天我们就借此先来说一下AtomicInteger,其他类型类的方法 实现方式都一样。
先对比一下没有使用前会引发的状况:
可以看到没有达到预想的效果,并且每次产生的结果都不一样,这就是上篇
文章所说到的,没有保证原子性,在执行+1操作时被其他线程插队,导致每次往主内存写入了相同的值。注:加上volatile也是会产生一样的结果!因为volatile不能保证原子性。
接下来我们使用AtomicInteger来试一下:
可以看到,达到了我们预期的效果。 那么他到底是是如何实现的呢? 我们来一探究竟!
在查看究竟前先讲解一个它的一个方法,以及涉及到的知识点,以便于后边的理解:
先说个点:CAS ==> Compare and Swap ==> 比较且交换
接下来 简单使用以下AtomicInteger提供的一个方法:
expert:期望值,即 期望改变的值
update:更改值,即 将期望值更改为什么
这里第一次我期望将初始值1更改为2,操作完成后 我再次期望将1 更改为2,我们看下执行结果:
第一次更改 成功 为 true 值变成了2,第二次执行失败 false 里边值还是2,第二次没有被更改过。这就是所谓的比较交换。
我们看下这个方法里边的实现:
this , valueOffset 下边会说是什么意思, expect , update 即是期望值和更改值上边有说
记住compareAndSwapInt()这个方法,CAS实现的 关键方法
我们先将目光转到自增方法getAndIncrement方法上看看底层如何实现:
解释下 这个 方法存在的内容:
unsafe:
由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
this:
表示当前AtomicInteger类。
valueOffset:
用一个图片说明,在AtomicInteger类有声明:
最后属性 1 是要增加的数值 这里是1。
让我们点进去再看看:
上边我们有看到 compareAndSwapInt 这个方法我们没有细说,这里说下:
解释下:
var1: 操作的对象
var2: 内存偏移量地址
var4:要增加的值
var5: 根据内存偏移量地址获取到的值 (上边提到的期望值)
var5 + var4 : 更改为的值
然后这里是个循环,先获取 当前内存偏移量位置 的属性值作为期望值,然后进行修改,如果过程其他线程已经改完了,那么修改返回值为false,则继续循环重新获取期望值,再次进行更改,直到修改成为止才退出循环。
1.根据传入对象和内存偏移量地址 拿取对应位置最新的值,为期望值
2.进行写操作,如果过程被其他线程更改,则期望值就会配对不上就会修改失败,继续循环直到成功。
可能 会有人问 这样操作进行修改过程中不会被打断吗?
对是的,不会被打断的,上边又说Unsafe类中的方法是可以直接访问计算机内存的,可以跟c语言一样。
这是在网上找的代码,内部在向CPU发送CAS指令时的汇编指令,是一条CPU并发原语,过程是原子的。
CAS并发语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。所以执行过程是不会被打断的,是线程安全的。
但是 会引发出来另一个问题切记: ABA问题
什么是ABA:
class ABA{
int i = 1;
}
假设此时有两条线程 操作i这个变量线程1,2 同时启动 进行 CAS 操作,他们读到的期望值都为 1接下来 两个线程执行以下过程: 线程1: 将 1 更改为 2 ,然后再将 2 更改为1 线程2:休息 5秒钟,将 1 更改为2 线程1 肯定比 线程2 先执行完,线程2 执行的时候是可以成功将1 更改为2的,但是有一个问题,在他更改的时候他不知道线程1 已经进行了多次更改,将1变为2又变为了1。
就像我桌子上的水被偷喝了,然后喝完又给我接了一杯,而我回来后却不知道已经被他人喝过了被他人占了个便宜。有种偷天换月的意思。
解释完之后给大家上个理论知识点:
CAS算法实现一个重要前提需要去除内存中某时刻的数据并立刻比较并替换,那么在这个时间出现时间差类会导致数据变化。
这就是很典型的ABA问题,那么如何解决呢?
可以加时间戳,版本号都可以解决:
AtomicStampedReference类:
上边初始化了 值为10 版本号为1的一个 AtomicStampedReference类,可以看到同样再调用compareAndSet方法的时候需要传4个值:分别为 期望值,修改值,期望版本号,修改版本号
有了版本号就可以避免CAS出现ABA的问题。
Atomic包里边不只是只有 Integer,Long等基本类型的原子类哦,自定义类同样可以原子操作:
可以通过AtomicReference类来操作
大家可以试试下边代码有时间的话:
总结一下:
为什么明明可以在 自增方法添加一个Synchronized关键字就可以解决为什么要通过原子类的CAS来解决。
Synchronized的比较笨重在上方例子,没必要杀鸡用牛刀,刚好也可以借助上方例子说一下CAS,他在使用时会将它修饰的代码块给锁住,其他线程不可以访问,会大大降低并发。
CAS 则可以大大提升并发,线程都可以同时执行,只不过是修改成功与否的问题了。
当然,这里说CAS也比较多也说一下它的缺点:
CAS虽然可以提升并发量,但容易给CPU造成很大的开销,并且也只能保证一个共享变量的原子性,对多个共享变量不能同时原子性。
可以关注下公众号。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。