Volatile

作者: hcq0514 | 来源:发表于2019-08-22 11:40 被阅读0次

复习内存模型

image.png

Volatile特性

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

保证可见性

public class VolatileTest {

    public static void main(String[] args) {
        test01();
    }

    /**
     * volatile可见性测试
     * todo 不加Thread.sleep(1000)会直接同步回主内存,待研究;
     */
    private static void test01() {
        Data data = new Data();
        new Thread(() -> {
            System.out.println("come in");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.setI(100);
            System.out.println("update complete");
        }, "t1").start();

        while (data.i == 0) {
        }
        System.out.println("data = " + data.i);
    }
}

class Data {
    int i = 0;
//    volatile int i = 0;

    void setI(int i) {
        this.i = i;
    }

    void addOne() {
        i++;
    }
}

输出结果


未加volatile的输出结果

由输出结果可见,main线程已经陷入了死循环,没有察觉到被data.i的值已经被线程t1修改了
将data的i值加上volatile后

class Data {
   volatile int i = 0;
    void setI(int i) {
        this.i = i;
    }
}
加了volatile的输出结果

由输出结果可见,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被其他线程感知到,其他线程的当前值作废,重新从主内存中获取值,这就叫可见性。

不保证原子性

package com.hcq.test.juc;

/**
 * @author : hcq
 * @date : 2019/8/14
 */
public class VolatileTest {

    public static void main(String[] args) {
      test02();
    }


    /**
     * volatile原子性测试
     */
    private static void test02() {
        Data data = new Data();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.addOne();
                }
            }).start();
        }
        // 默认有 main 线程和 gc 线程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(data.i);
    }
}

class Data {
    volatile int i = 0;
    void addOne() {
        i++;
    }
}

输出结果

19926

Process finished with exit code 0

经多次测试发现,我们的期望值是20000,但是都不会达到2W,这是因为i++他在系统底层其实是三个语句

public void addOne();
    addOne:
       0: getstatic     #2                  // Field count:I  获取值
       3: iconst_1      
       4: iadd                                  // 值增加
       5: putstatic     #2                  // Field count:I  值保存
       8: return        
}

可以用synchronized跟atomic原子变量解决。

禁止指令重排

计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种

  1. 编译器优化的重排
  2. 指令并行的重排
  3. 内存系统的重排

单线程环境里面确保程序最终执行的结果和代码执行的结果一致
处理器在进行重排序时必须考虑指令之间的数据依赖性

public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;
    
    public void method01() {
        a = 1;           // flag = true;
                         // ----线程切换----
        flag = true;     // a = 1;
    }

    public void method02() {
        if (flag) {
            a = a + 3;
            System.out.println("a = " + a);
        }
    }

}

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测
由于method01的两条语句a = 1; flag = true; 没有依赖性,CPU有可能会重排(flag = true,a = 1;),调换了位置,此时如果另一个线程在执行method02,在flag = true执行完,a=1还没执行的时候插了进来,就会导致获取的值为0,输出3;

volatile应用单例实例

package com.hcq.test.juc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Singleton01 {
    private static Singleton01 instance = null;
    private Singleton01() {
        System.out.println(Thread.currentThread().getName() + "  construction...");
    }
    public static Singleton01 getInstance() {
        if (instance == null) {
            instance = new Singleton01();
        }
        return instance;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.execute(Singleton01::getInstance);
        }
        executorService.shutdown();
    }
}
输出结果
Connected to the target VM, address: '127.0.0.1:59395', transport: 'socket'
pool-1-thread-3  construction...
pool-1-thread-1  construction...
pool-1-thread-2  construction...
Disconnected from the target VM, address: '127.0.0.1:59395', transport: 'socket'

这个单例因为可见性问题会导致多次初始化
解决方法之一: 双重锁,

package com.hcq.test.juc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Singleton02 {
    private static volatile Singleton02 instance = null;

    private Singleton02() {
        System.out.println(Thread.currentThread().getName() + "  construction...");
    }

    public static Singleton02 getInstance() {
        if (instance == null) {
            synchronized (Singleton01.class) {
                if (instance == null) {
                    instance = new Singleton02();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(Singleton02::getInstance);
        }
        executorService.shutdown();
    }
}

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

为什么要用双层锁,单单用volatile不行吗。

相关文章

网友评论

      本文标题:Volatile

      本文链接:https://www.haomeiwen.com/subject/pgigsctx.html