java多线程与高并发(三)volatile与CAS

1.volatile关键字原理

用 volatile 关键字修饰的共享变量,编译成字节码后增加 Lock 前缀指令,该指令要做两件事:

  1. 将当前工作内存缓存行的数据立即写回到主内存。
  2. 写回主内存的操作会使其他工作内存里缓存了该共享变量地址的数据无效(缓存一致性协议保证的操作)。

Lock前缀指令还有内存屏障作用:

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的(即在执行到共享变量时,因为内存屏障的存在,会把它前面的操作都写回主内存,然后再执行共享变量的操作,对共享变量的操作一执行完也会写回主内存)。

1.1.具有原子性吗?

 public class Test {
        public volatile int inc = 0;

        public void increase() {
            inc++;
        }

        public static void main(String[] args) {
            final Test test = new Test();
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    public void run() {
                        for (int j = 0; j < 1000; j++) test.increase();
                    }

                    ;
                }.start();
            }
            while (Thread.activeCount() > 1) //保证前面的线程都执行完            Thread.yield();        System.out.println(test.inc);    
                
        }}

当线程安全时,输出结果是 10000,但是多次运行结果都是一个小于 10000 的值。因为 inc++ 不是一个原子性操作,它包括读取变量,进行加1操作,写入工作内存。

加入线程1读取到 inc=100 的值,然后进行加1操作被阻塞,这时线程上下文切换,线程2读取也读取到 inc=100 的值,然后进行加1操作,写入工作内存,最后写入主内存。由于线程1被阻塞到第二步操作,即使工作内存缓存行失效,它也不会重新读取 inc 的值。所以这时出现线程安全问题。

说明 volatile 关键字不具有原子性。

1.2.具有可见性吗?

Lock 前缀指令保证当工作内存中缓存行数据写会主内存时,会使其他工作内存的缓存行无效,会重新读取主内存中数据。

说明 volatile 关键字具有可见性。

1.3.具有有序性吗?

Lock 前缀指令有内存屏障的作用。

一共有4种内存屏障,分别是 LoadLoad、LoadStore、StoreStore、StoreLoad。

  • LoadLoad:确保 Load1 数据的读取先于 Load2 的数据及所有后续数据的读取。
  • StoreStore:确保 Store1 数据的写回先于 Store2 数据及所有后续数据的写回。
  • LoadStore:确保 Load1 数据的读取先于 Store2 数据及所有后续数据的写回。
  • StoreLoad:确保 Store1 数据的写回先于 Load2 数据及所有后续数据的读取。

JMM 插入内存屏障保守策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障,后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障和一个 LoadStore 屏障。
0a1aa22f431a9232811c5aa6edbb237b.png

由于 JMM插入内存屏障保守策略增加了很多内存屏障, 增加很多开销,性能会下降,所以很多处理器对内存屏障的插入进行了优化。如 X86/X64 处理器,只会在写操作后面增加一个 storeLoad 内存屏障。

f52ebd3d8aeb8d042ead84bc64c1b67d.png

虽然只有 StoreLoad 一个内存屏障了,但是还是保证了有序性。

说明 volatile 关键字具有有序性。

1.4.DCL单例

何为DCL,DCL即Double Check Lock,双重检查锁定。下面从几个单例模式来讲解

懒汉式

public void Singleton{
    private static Singleton singleton;
 
    private Singleton(){}
 
    public static Singleton getInstance(){
        if(singleton==null){
               singleton=new Singleton();
        }
 
            return singleton;
    }
 
        
 
}

这种方法在单线程下是可取的,但是在并发也就是在多线程的情况下是不可取的,因为其无法保证线程安全,优化如下:

public void Singleton{
    private static Singleton singleton;
 
    private Singleton(){}
 
    public synchronized static Singleton getInstance(){
        if(singleton==null){
               singleton=new Singleton();
        }
 
        return singleton;
    }
 
}

优化非常简单,在getInstance方法上加上了synchronized同步,尽管jdk6以后对synchronized做了优化,但还是会效率较低的,性能下降。那该如何解决这个问题?于是有人就想到了双重检查DCL

public void Singleton{
    private static Singleton singleton;
 
    private Singleton(){}
 
    public static Singleton getInstance(){
        if(singleton==null){
            synchronized(Singleton.class){
                if(singleton==null)  singleton=new Singleton();
            }
              
        }
        return singleton;
    }
 
}

这个代码看起来perfect:

如果检查第一一个singleton不为null,则不需要执行加锁动作,极大的提高了性能
如果第一个singleton为null,即使有多个线程同时判断,但是由于synchronized的存在,只有一个线程能创建对象
当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象
DCL看起来非常完美,但其实这个是不正确的。逻辑没问题,分析也没问题?但为何是不正确的?不妨我们先回顾一下创建对象的过程

为对象分配内存空间
初始化对象
将内存空间的地址赋值给对应的引用
但由于jvm编译器的优化产生的重排序缘故,步骤2、3可能会发生重排序:

为对象分配内存空间
将内存空间的地址赋值给对应的引用
初始化对象
如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象

知道问题的原因,那么我们就可以解决?

不允许重排序

重排序不让其他线程看到

解决方法
利用volatile的特性即可阻止重排序和可见性

public class Singleton {
   //通过volatile关键字来确保安全
   private volatile static Singleton singleton;
 
   private Singleton(){}
 
   public static Singleton getInstance(){
       if(singleton == null){
           synchronized (Singleton.class){
               if(singleton == null){
                   singleton = new Singleton();
               }
           }
       }
       return singleton;
   }
}
类初始化的解决方案

public class Singleton {
   private static class SingletonHolder{
       public static Singleton singleton = new Singleton();
   }
 
   public static Singleton getInstance(){
       return SingletonHolder.singleton;
   }
}

1.5.总结

volatile主要有两个作用:
1.线程可见性
2.防止cpu指令重排

2.CAS

由于 volatile 关键字不具有原子性,所以一般在使用 volatile 关键字的地方,常常出现 CAS。

CAS是 Compare And Swap,它和 volatile 关键字都是实现 JUC 的基础,其中 java.util.concurrent.atomic 核心都是 CAS 。

使用 CAS 有两个核心参数,第一个是旧值,第二个是期望值。根据当前类(this)和 内存偏移(valueOffset)计算出内存中的值,当内存中的值和旧值相等时,更新为新值并返回 true ,否则返回 false。

比如 AtomicInteger 类中的 CompareAndSet() 方法:

public final boolean compareAndSet(int expect, int update) {    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}

根据 this 和 valueOffset 计算出的值与 expect 是否相等,相等把内存中的值更新为 update 并返回 true ,否则返回 false 。

2.1带来的其他问题?

CAS有三个问题:

  1. ABA问题
  2. 循环时间长开销大
  3. 只能保证一个变量的原子操作

2.1.1ABA问题

ABA问题描述的是当 CAS 更新一个值原来是 A,变成了 B ,又变成了 A,那么再使用 CAS 进行检查时会发现值没有发生变化,但是实际上却变化了。

利用乐观锁加版本号解决。
如果是基础类型:无所谓,不影响结果值
如果是引用类型:就像你的女朋友和你分手之后又复合,中间经历了别的男人

2.1.2循环时间长开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。

利用自旋次数或者超时时间解决。

2.1.3只能保证一个变量的原子操作

对多个变量使用 CAS 保证不了原子性。

利用锁或者 JDK1.5 提供的 AtomicReference 类解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容