Volatile学习

3 篇文章 0 订阅
2 篇文章 0 订阅

Volatile

1 JMM

1.1 JMM定义

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

1.2 JMM关于同步的规定

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JMM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存中拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

线程间通信访问过程

2 Volatile的特性

2.1 特性

Volatile是Java虚拟机提供的轻量级的同步机制

  1. 可见性
  2. 原子性
  3. 有序性
2.1.1 可见性

各个线程对主内存中共享变量的操作都是在各个线程各自拷贝到自己的工作内存后进行操作再写回主内存的,这就可能存在一个线程A修改了共享变量A的值但还未写入到主内存时,另外一个线程B又对主内存中同一个共享变量A进行操作,但此时线程A工作内存中共享变量A对线程B来说并不可见。这种工作内存与主内存同步延迟现象就造成了可见性问题。

对象A被volatile关键字修饰后,若对象A被线程A修改后,会立马将值写入主内存中,并通知其它线程,其它线程也会立马接收到最新值。如下demo

/**
 * @Author:于晨
 * @Description:volatile可见性Demo
 * @Date:Create in 2021/3/17 23:15
 */
public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        VisbilityData data = new VisbilityData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            // 线程等待一会,修改值
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update num value:" + data.num);
        }, "AA").start();
        while (data.num == 0) {
            // 线程循环等待,直到number值不再等于0,即通知其它线程数字已改变
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over,MyData.number=" + data.num);
    }
}

/**
 * 1.num变量没有添加volatile关键字,验证普通变量没有可见性
 */
class VisbilityData{
    //int num = 0; // v1
    volatile int num = 0; // v2

    public void addTo60(){
        this.num = 60;
    }
}
v1最终结果输出:
    AA	 come in
	AA	 update num value:60
v2最终结果输出:
    AA	 come in
	AA	 update num value:60
	main	 mission is over,MyData.number=60
2.1.2 原子性

eg:两个线程A、B对volatile修饰的变量X进行X++操作。X++的过程可以分为三步:获取X的值;对X值进行+1;将X的新值写入到缓存中。

线程A首先获得X的值,进行了修改值的操作,此时线程B挂起,接着线程B突然唤醒抢到了CPU的调度权,线程B还没来得及读取最新的值就已经对X值进行了操作,并写入了缓存中,造成数据丢失。所以volatile不能保证原子性

/**
 * @Author:于晨
 * @Description:volatile不保证原子性Demo
 * @Date:Create in 2021/3/17 23:42
 */
public class VolatileAtomicityDemo {
    public static void main(String[] args) {
        AtomicData data = new AtomicData();
        for(int i = 0; i < 20; i++) {
            new Thread(() -> {
                for(int j = 0; j < 1000; j++) {
                    data.addData();
                }
            }, String.valueOf(i)).start();
        }
        // 等计算线程全部结束后,再获取最终结果值查看结果,期望值2w
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " finally num = " + data.num);
    }
}

class AtomicData {
    // num前加了volatile关键字,验证volatile修饰的变量不保证原子性
    volatile int num = 0;

    public void addData() {
        num++;
    }
}
最终输出结果:19761
2.2.2.1 如何保证原子性
  1. 使用synchronized关键字
  2. 使用concurrent包下的Atomic修饰的原子变量(CAS)
2.1.3 有序性

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

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序(指令重排序只保证串行语义的执行的一致性(单线程),并不会保证多线程之间的语义一致性),一般如下3种:

指令重排序

单线程环境中确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

demo1:

int a = 1; // 1
int b = 2; // 2
a = a + 1; // 3
b = a * a; // 4
// 经过编译器重排序后,可能的执行顺序有 
// 1234 2134 1324 但是不可能将4放在第一步执行
// 因为处理器在进行重排序时必须要考虑指令之间的数据依赖性。

demo2:

/**
 * @Author:于晨
 * @Description:volatile禁止指令重排
 * @Date:Create in 2021/3/18 0:52
 */
public class VolatileOrder {
    int x = 0; 
    boolean bool = false;

    void setData() {
        x = 1; // 1
        bool = true; // 2
    }

    void calculateData() {
        if(bool) {
            x = x + 2;
        }
    }
}
// 单线程情况下执行顺序是12
// 多线程情况下由于指令重排的问题,在不涉及数据依赖性的情况下,不能确定变量的执行顺序
// 多线程环境中线程交替执行,由于编译器优化重排的存在,致使两个线程中使用的变量是否能保持一致性是未知的(由于都是对变量赋值且不两个变量不涉及数据依赖,执行顺序12或者21是未知的),导致结果无法预测。
// volatile可以禁止指令重排

3 内存屏障

内存屏障(Memory Barrier):内存栅栏,是一个cpu指令,作用有两个:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

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

内存屏障

4 单例模式在多线程下可能出现的问题

单例模式demo(单线程)

/**
 * @Author:于晨
 * @Description:单例模式demo
 * @Date:Create in 2021/3/18 1:24
 */
public class SingletonDemo {
    private static SingletonDemo instance = null;

    // v1 懒汉式:第一次调用才初始化,避免内存浪费。存在线程安全问题
    /*
    private static SingletonDemo instance;
    
    private static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }*/
    
    // v2 饿汉式:类加载时就初始化,浪费内存。不存在线程安全问题
    /*
    private static SingletonDemo instance = new Singleton();
    
    private static SingletonDemo getInstance() {
        instance = new SingletonDemo();
        return instance;
    }*/
    
    SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "SingletonDemo被创建了");
    }
    
    // v3 双端解锁机制
    private static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized(SingletonDemo.class);
            if (instance == null) {
            	instance = new SingletonDemo();
            }
        }
        return instance;
    }

    public static void main(String[] args){
        // 单线程下对象只被创建一次
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        // 多线程下,对象可能会被创建多次
        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
// v1 单线程执行结果,创建的是同一个对象
1 SingletonDemo被创建了
true
true
// v1 多线程执行结果,创建了多个对象
1 SingletonDemo被创建了
4 SingletonDemo被创建了
6 SingletonDemo被创建了
2 SingletonDemo被创建了
// 多线程解决方案V2 多线程执行结果,创建的是同一个对象
1 SingletonDemo被创建了

双端解锁机制不一定是线程安全的,原因是有指令重排序的存在,完成初始化对象时,有以下三个步骤

  1. memory = allocate(); // 给对象分配内存空间
  2. instance(memory); // 初始化对象
  3. instance = memory; // 设置instance指向刚分配的内存地址,此时instance != null

步骤2和步骤3不存在数据依赖关系,并且无论重排前还是重排后的程序执行结果在单线程中都一致,所以这种优化重排是允许的。

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

扩展

1 CAS

CAS(CompareAndSwap):

比较并交换(比较当前工作内存中的值和主内存中的值,如果相同执行规定操作;否则继续比较直到主内存和工作内存中的值一致为止。)。是一条CPU并发原语,CAS汇编指令是一种完全依赖于硬件的功能,通过它实现了原子操作。**原语的执行是连续的,在执行过程中不允许被中断,即不会造成数据不一致的问题。

优点: 确保线程安全,在对值进行操作时,将工作内存中的值与主内存中的值进行比较,若一致则交换,若不一致则重新从主内存中拷贝副本到工作内存,不交换要修改的值。

缺点:

  • 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
  • 只能保证一个共享变量的原子操作。
  • 引出ABA问题

JUC包下的Atomic修饰的类是用到CAS保证数据一致性的。

例如:AtomicInteger.compareAndSet(期望值,交换值),若期望值与主内存中值一致,则返回true,并将工作内存中和主内存中的值替换为交换值。

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        System.out.println(atomicInteger.compareAndSet(1, 10) + " " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 20) + " " + atomicInteger.get());
    }
}
// 结果
true 10
false 10

Unsafe是CAS的核心类。Unsafe类中的方法都是native修饰的,即Unsafe类中的方法都直接调用操作系统底层资源执行响应任务。

由于Java方法无法直接访问操作系统底层系统,需要通过**本地方法(native)**来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.msic包中,内部方法操作可以像C的指针一样直接操作内存。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

valueOffset:变量在内存中的偏移地址,调用objeckFieldOffset获取真实的物理内存地址

value:volatile修饰,确保数据对其它线程可见

AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.getAndIncrement();

// 调用unsafe的getAndAddInt方法,this:当前对象,valueoffset:当前对象物理内存地址,1:自增量
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// unsafe的方法,var1:当前对象,var2 当前对象物理内存地址,var4:自增量
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 获取当前对象这个地址上的值,即获取主内存中当前变量的值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 比较当前对象这个内存地址上的值和之前获取到的值是否一致,若一致,返回值+自增量的值,若不一致则一直循环。
    return var5;
}

为什么Atomic用CAS而不是用synchronized?

  • CAS既保证数据一致性又保证并发性
  • synchronized同一时间只有一个线程能拿到锁,不能保证并发性

2 ABA问题

CAS算法实现一个重要前提需要提取出内存中某个时刻的数据并在当下时刻比较并替换,那么这个时间差内会导致数据的变化。

线程A从内存位置X取出数据Y,另外一个线程B也从内存位置X取出数据Y,并且线程B进行了一些操作将Y改成了Z然后又将Z改成了Y,这时候线程A进行CAS操作发现内存中仍然是Y,然后线程A操作成功。

尽管线程A的CAS操作成功,但并不代表这个过程就是没有问题的。

3 原子引用

对一个对象进行CAS操作,可以使用AtomicReference类实现

public class ReferenceDemo {
    public static void main(String[] args){
        User user1 = new User("张三", 24);
        User user2 = new User("李四", 22);
        AtomicReference<User> reference = new AtomicReference<>();
        reference.set(user1);
        System.out.println(reference.compareAndSet(user1, user2));
        System.out.println(reference.compareAndSet(user2,user1));
    }
}

@Data
@AllArgsConstructor
class User {
    String name;

    int age;
}
// 结果
true
true

4 解决ABA问题

为了解决ABA问题,可以使用 原子引用 + 版本号(类似时间戳)

第一次:线程A、B分别获取变量值和版本号

主内存version线程Aversion线程Bversion
111111

第二次:线程A进行CAS操作修改了变量的值为2,每次修改版本号的值+1

主内存version线程Aversion线程Bversion
222211

第三次:线程A进行CAS操作修改了变量的值为1,每次修改版本号的值+1

主内存version线程Aversion线程Bversion
131311
public class StampReferenceDemo {
    static AtomicReference<Integer> integerAtomic = new AtomicReference<>(100);

    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        // *****************ABA问题产生
        new Thread(() -> {
            integerAtomic.compareAndSet(100, 101);
            integerAtomic.compareAndSet(101, 100);
        }, "threadA").start();

        new Thread(() -> {
            // 确保threadA执行完成
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "是否修改成功:" +
                    integerAtomic.compareAndSet(100, 101) + " " + integerAtomic.get());
        }, "threadB").start();

        // *****************ABA问题解决
        new Thread(() -> {
            int stamp = stampedReference.getStamp(); // 版本号
            System.out.println(Thread.currentThread().getName() + "第1次操作版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stampedReference.compareAndSet(100, 101,
                    stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第2次操作版本号:" + stampedReference.getStamp());
            stampedReference.compareAndSet(101, 100,
                    stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第3次操作版本号:" + stampedReference.getStamp());
        }, "threadC").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp(); // 版本号
            System.out.println(Thread.currentThread().getName() + "第1次操作版本号:" + stampedReference.getStamp());
            // 确保ThreadC完成了CAS操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean bool = stampedReference.compareAndSet(100, 101,
                    stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + " CAS操作是否成功:" + bool);
            System.out.println("当前最新版本号为::" + stampedReference.getStamp() +  "当前变量的值为:" + stampedReference.getReference());
        }, "threadD").start();
    }
}
// 结果
threadC第1次操作版本号:1
threadD第1次操作版本号:1
threadC第2次操作版本号:2
threadB是否修改成功:true 101
threadC第3次操作版本号:3
threadD CAS操作是否成功:false
当前最新版本号为::3当前变量的值为:100
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值