volatile | CAS| ABA

JMM

JMM(Java内存模型Java Memory Model)是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  ①线程解锁前,必须把共享变量的值刷新回主内存;

  ②线程解锁前,必须读取主内存的最新值到自己的工作内存;

  ③加锁解锁是同一把锁;

①把变量读到各个线程的工作内存中;②线程运算完之后把变量的值改好,然后把它写回主内存;③可见性--让其他线程马上知道这个值;

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的;

这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题;

 可见性

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\tcome in");
            //暂停一会线程
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
            myData.add();
            System.out.println(Thread.currentThread().getName() + "\tupdated number value:" + myData.num);

            },"aa").start();
            //第二个线程就是main线程
        while (myData.num == 0){
            //main一直在循环,等待num的值不为0
        }
        System.out.println(Thread.currentThread().getName() + "\tmission is over, main get num value:" + myData.num);

    }
}  //volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改;如果不加volatile则主线程就一直在那等待着,程序就卡那了; aa come in  aa	updated number value:60 main	mission is over, main get num value:60
 

不保证原子性

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile关键字,不保证原子性
    public void addPlusPlus(){
        num++;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最后结果应该是20000
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20个线程计算完之后,再用main线程取得最后结果
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
    }
}

main     finally num is:19951 结果是随机的;

 n++的字节码过程

public class T1 {
    volatile int n = 0;
    public void add(){
        n++; //n++被拆分成3个指令:①执行getfield拿到原始值n;②执行iadd进行加1操作;③执行putfield把累加后的值写回;
    }
}
  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field n:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field n:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:

不保证原子性的解决方法:

class MyData{
     volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile关键字,不保证原子性
    public void  addPlusPlus(){
        num++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAutomic(){
        atomicInteger.getAndIncrement();   //如何解决原子性,加sync...,它锁的范围太广了;使用JUC下的AtomicInteger
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        //TODO 原子性的解决方法
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最后结果应该是20000
                    myData.addMyAutomic();
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20个线程计算完之后,再用main线程取得最后结果
        while (Thread.activeCount() > 2){ //如果线程活跃个数>2,就暂停当前线程,把CPU资源让出
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally num is:" + myData.atomicInteger);

    }
}

main finally num is:19356
main AtomicInteger type,finally num is:20000

指令做重排

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;
    //构造方法,都会执行main    我是构造方法SingletonDemo
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t我是构造方法SingletonDemo");
    }
    //单例模式,在高并发下,还是会出异常,因为存在指令重排  DCL(Double Check Lock)双端检索机制,加锁前后都做判断 
    public static  SingletonDemo getInstance(){
/*        if (instance == null){
             instance = new SingletonDemo(); //单例模式加synchronized太重,需要控制的只是这一行
        }
        return instance;
        //在上边代码会出现以下情况:
        2    我是构造方法SingletonDemo
        4    我是构造方法SingletonDemo
        3    我是构造方法SingletonDemo
        1    我是构造方法SingletonDemo*/

        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance; //这里只是拿到了内存地址,数据还没拿到,还是有可能发生指定重排的;加volatile可实现禁止指令重排
    }
    public static void main(String[] args) {
        //单线程下
/*        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true*/

        //并发环境下,情况发生了很大变化
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}--------------------------1 我是构造方法SingletonDemo

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象;

概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它有两个作用:

  ①保证特定操作的执行顺序;

  ②保证某些变量的内存可见性(利用该特性实现volatile的内存可见性);

由于编译器和处理器都能执行指令重排。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各自CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

为了保证有序,需要禁止指定重排;

线程安全性获得保证:

工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于执行重排导致的可见性问题和有序性问题,可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

volatile只能保证其中的两个,不保证原子性;所以是低配版的轻量级;

你在哪些地方用过volatile? ①单例模式DCL代码

单例模式一共6种;懒汉、饿汉

并发

 DCL(双端检锁)机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排;

原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

instance = new SingletonDemo();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间

instance(memory);//2.初始化对象

instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! = null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是运行的。

memory = allocate();//1.分配对象内存空间

instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! = null,但是对象还没有初始化完成!

instance(memory);//2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

单例模式DCL代码;单例模式volatile分析

1Unsafe

  是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

Unsafe类存在于sun.misc包中,其中内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

3.

它时来判断内存中的某个值是否为期望值;

CAS缺点:

CAS没有加锁,要保证数据一致性、原子性,使用do while

引导出ABA

Unsafe类和CAS思想(自旋)

AtomicInteger--->

ABA狸猫换太子

ABA,只管结果不管过程,只要头跟尾对上就可以了;

解决ABA问题?

AtomicReference原子引用
@Getter
@ToString
@AllArgsConstructor
class User{
    String username;
    int age;

}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User kris = new User("kris", 22);
        User alex = new User("alex", 23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(kris); //如果想对某个类进行原子性包装
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());

/*      true    User(username=alex, age=23)
        false    User(username=alex, age=23)

        */
    }
}

时间戳的原子引用  就是解决ABA问题

ABA问题的解决:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo { //ABA问题的解决 AtomicStampedReference
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    //这两个要写这里
    public static void main(String[] args) {

        System.out.println("============ABA问题的产生============");
        new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 101);
            },"t1").start();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicReference.compareAndSet(101, 2019) + "\t" + atomicReference.get());

            },"t2").start(); //true     2019  成功改为了2019,但是中间是有人动过的;
        //暂停一会线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("==================ABA问题的解决#############");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);//1
            //暂停1s t3线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());},
                "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);//1
            //暂停3s 4线程保证上面的t3线程完成了一次ABA操作;t3 t4都暂停下就是让它们的起点一样,都拿到版本号为1的数据 100
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean restlt = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); //t4提交的版本号2,但服务器上最新的版本号是3;
            System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + restlt + "\t当前最新版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:" + atomicStampedReference.getReference());
            },"t4").start();
    }
}

============ABA问题的产生============
true    2019
==================ABA问题的解决#############
t3    第1次版本号:1
t4    第1次版本号:1
t3    第2次版本号:2
t3    第3次版本号:3
t4    修改成功否:false    当前最新版本号:3
t4    当前实际最新值:100

原文地址:https://www.cnblogs.com/shengyang17/p/10765881.html

时间: 2024-10-12 10:03:56

volatile | CAS| ABA的相关文章

CAS / ABA

CAS / ABA 标签(空格分隔): 操作系统 1. CAS 解决 Volatile 不保证原子性的问题 /** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int ge

CAS ABA问题

接触并发编程少不了CAS,这里不讲CAS,在另一篇文章里面有写CAS,这里只关注CAS的ABA问题. 什么叫CAS的ABA问题? 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了. 比如线程A操作值a,将值修改为b,这时线程B也拿到了a的值,将a改为b,再改为a,这时线程A比对值的时候,发送值还是和expect的一样,就会继续操作.如下面代码,T2的CAS

Java CAS ABA问题发生的场景分析

提到了CAS操作存在问题,就是在CAS之前A变成B又变回A,CAS还是能够设置成功的,什么场景下会出现这个问题呢?查了一些资料,发现在下面的两种情况下会出现ABA问题. 1.A最开始的内存地址是X,然后失效了,有分配了B,恰好内存地址是X,这时候通过CAS操作,却设置成功了 这种情况在带有GC的语言中,这种情况是不可能发生的,为什么呢?拿JAVA举例,在执行CAS操作时,A,B对象肯定生命周期内,GC不可能将其释放,那么A指向的内存是不会被释放的,B也就不可能分配到与A相同的内存地址,CAS失败

Java并发编程原理与实战四十三:CAS ---- ABA问题

CAS(Compare And Swap)导致的ABA问题 问题描述 多线程情况下,每个线程使用CAS操作欲将数据A修改成B,当然我们只希望只有一个线程能够正确的修改数据,并且只修改一次.当并发的时候,其中一个线程已经将A成功的改成了B,但是在线程并发调度过程中尚未被调度,在这个期间,另外一个线程(不在并发中的请求线程)将B又修改成了A,那么原来并发中的线程又可以通过CAS操作将A改成B 测试用例: public class AbaPro { private static final Rando

CAS -- ABA问题的解决方案

我们现在来说什么是ABA问题.假设内存中有一个值为A的变量,存储在地址V中. 此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差.线程1和线程2已经获取当前值,线程3还未获取当前值. 接下来,线程1先一步执行成功,把当前值成功从A更新为B:同时线程2因为某种原因被阻塞住,没有做更新操作:线程3在线程1更新之后,获取了当前值B. 在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A. 最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A

J.U.C atomic 数组,字段原子操作

这里看一下原子数组操作和一些其他的原子操作. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray的API类似,选择代表性的AtomicIntegerArray来描述这些问题. int get(int i) //获得当前位置的值 void set(int i, int newValue) //设置给定位置的值 void lazySet(int i, int newValue) int getAndSet(int i, int newVal

Java中并发问题整理

1. java中有几种方法可以实现一个线程? 使用Runnable,Callable,Thread或者线程池 2. 如何停止一个正在运行的线程? 可以使用正在运行的线程,支持线程中断,通常是定义一个volatile的状态变量,在运行线程线程中读这个变量,其它线程中修改这个变量 3.sleep和wait有什么区别 sleep方法是在指定的时间内让正在执行的线程暂停执行,但不会释放锁.而wait方法是让当前线程等待,直到其他线程调用对象的notify或notifyAll方法.wait方法会释放掉锁,

java并发编程之原子操作

先来看一段简单的代码,稍微有点并发知识的都可以知道打印出结果必然是一个小于20000的值 package com.example.test.cas; import java.io.IOException; /** * @author hehang on 2019-10-09 * @description */ public class LockDemo { private volatile int i; public void add(){ i++; } public static void m

JUC 中的 Atomic 原子类总结

1 Atomic 原子类介绍 Atomic 翻译成中文是原子的意思.在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的.在我们这里 Atomic 是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰. 所以,所谓原子类说简单点就是具有原子/原子操作特征的类. 并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示. 根据操作的数据类型,可以将JUC包中