volatile关键字

复制代码
volatile可以保证变量的可见性。
这里的变量包括类变量、实例变量,但不包括局部变量和方法参数,因为后者是线程私有的,不存在线程竞争问题

java内存模型(JMM)规定,所有变量都存储在主内存中,同时每个线程还有自己的工作内存。
线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主存中的数据
不同线程也无法访问对方工作内存中的变量,线程间变量值的传递需要通过主存来完成(引自周志明《深入理解Java虚拟机》)

volatile可以保证
(1)任何线程更新volitale修饰的变量后都会立即同步到主存中
(2)线程每次读取volitale修饰的变量,都必须到主存中取最新值
以上亮点保证了“可见性”,即任意线程对变量的修改能立即被其他线程所知
java 复制代码
package com.concurrent;

public class VolatileDemo {

    // 如果不加volatile,你会发现线程2已经结束很久了,线程1还在死循环。
    // 但是一旦加上volatile,线程2一执行,线程1会立刻跳出循环 
    // 这是因为volatile可以保证变量的可见性。
    // 这里的变量包括类变量、实例变量,但不包括局部变量和方法参数,因为后者是线程私有的,不存在线程竞争问题
    // java内存模型(JMM)规定,所有变量都存储在主内存中,同时每个线程还有自己的工作内存。
    // 线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主存中的数据,
    // 不同线程也无法访问对方工作内存中的变量,线程间变量值的传递需要通过主存来完成。
    // volatile可以保证
    // (1)任何线程更新volitale修饰的变量后都会立即同步到主存中
    // (2)线程每次读取volitale修饰的变量,都必须到主存中取最新值
    // 以上亮点保证了"可见性",即任意线程对变量的修改能立即被其他线程所知
    //
    //public static Integer money = 1000;
    public volatile static Integer money = 1000;

    public static void main(String[] args) throws InterruptedException {
        //线程1
        new Thread(() -> {
            while (money == 1000) {

            }
            System.out.println("存款已经不是" + money + "了");
        }).start();

        Thread.sleep(2000);

        //线程2
        new Thread(() -> {
            money = 900;
            System.out.println("存款现在是" + money);
        }).start();
    }
}

但是,volatile并不能保证原子性。下述代码如果能保证原子性的情况下应该返回200000,但实际执行后打印的结果远小于这个值。这是因为race++这个看似简单的操作实际上包含3个步骤:

(1)从主存获取值

(2)执行加1(iconst_1,iadd指令)

(3)写回主存

在线程执行iconst_1,i_add这些指令的时候,其他线程可能已经把race的值改变了,因此当前内存写回主存时就可能覆盖掉最新的值而把老值加1的结果写回去。(此处代码和解释均引自周志明《深入理解Java虚拟机》)

java 复制代码
package com.concurrent;

public class VolatileDemo2 {

    public static volatile int race = 0;

    public static void increase(){
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    increase();
                }

            });
            threads[i].start();
        }

        while(Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(race);
    }
}

同时,volatile能防止指令重排。所谓指令重排,就是虚拟机在执行执行时,不考虑并发的影响,再保证结果不变的情况下,将部分指令重新排序的现象,也就是所谓的"线程内表现为串行"(With-Thread as-if-serial semantics).

一个典型的例子是懒汉是单例的双重检测锁模式。如果不加volatile修饰,由于new对象并非原子操作,就有可能出现指令重排的现象。new对象分为三个步骤(1)分配内存空间 (2)执行构造方法,初始化对象 (3)把对象指针执行这片空间。指令重排后原本(1)(2)(3)的顺序可能变成(1)(3)(2),这样有可能线程A执行完(1)(3),正要执行(2)的时候,线程B抢到执行权,判断lazyMan == null,为false,于是返回对象,但是此时的对象还并没有初始化,这时候去使用此对象显然会有问题。比如这里的使用就是打印,那就有可能出现先打印对象,然后再执行构造方法中的打印。但是,经过多次尝试,并没有出现1次这个问题,是概率太小还是理论有误?

java 复制代码
package com.concurrent;

public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    //private static LazyMan lazyMan = null;
    private static volatile LazyMan lazyMan = null;
    

    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }

        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    LazyMan lazyMan = LazyMan.getInstance();
                    System.out.println(lazyMan);
                }
            }).start();
        }
    }


}
相关推荐
小园子的小菜几秒前
Rockect基于Dledger的Broker主从同步原理
java·开发语言
鹿屿二向箔2 分钟前
【论文+源码】创建一个基于Spring Boot的体育场管理系统
java·spring boot·后端
漫无目的行走的月亮2 分钟前
Spring boot实现图片上传和下载
java·spring boot
火云牌神14 分钟前
[python]实现可以自动清除过期条目的缓存
开发语言·python·缓存
Libby博仙16 分钟前
asp.net core Web Api中的数据绑定
java·前端·asp.net
小金的学习笔记22 分钟前
SpringBootWeb案例-2
java·服务器·springboot·web
杰九25 分钟前
【全栈】SprintBoot+vue3迷你商城(2)
java·数据库·spring boot·mysql
华年源码25 分钟前
基于springboot的课程作业管理系统(源码+数据库+文档)
java·数据库·毕业设计·源码·springboot
黄霑和金庸我都喜欢30 分钟前
桌面开发 的设计模式(Design Patterns)核心知识
开发语言·后端·golang
晚雾也有归处32 分钟前
结构体(C语言)
c语言·开发语言·数据结构·算法