当前位置:   article > 正文

java并发编程之 volatile关键字

java并发编程之 volatile关键字
1、简单介绍一下JMM

Java 内存模型(Java Memory Model 简称JMM)是一种抽象的概念,并不真实存在,指一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

JMM规定:

  1. 所有的共享变量都存储在主内存中(包括实例变量、类变量,静态变量,但是不包括局部变量,因为局部变量是线程私有的,不存在多线程之间的竞争)
  2. 每个线程都有自己的工作内存,线程工作内存中保留了被线程使用的共享变量的副本
  3. 线程对变量的操作(读或者写)都必须在工作内存中完成,不能直接操作主内存
  4. 不同线程之间不能相互访问对方的工作内存,线程间变量值的传递需要通过主内存完成。

工作内存和主内存的关系图:

2、分析一下共享变量的不可见性问题。

看代码,定义了一个成员变量 falg,一个子线程负责修改flag的值,另外一个子线程根据flag的值判断是否跳出空循环,实际执行结果为下图,可见,线程0对flag的修改并没有影响到线程1;这就是多线程下共享变量的修改会存在不可见性

原因就是Thread-1一直访问的都是自己本地内存中的flag,而没有从主内存中去更新flag,所以没办法跳出循环。

  1. public class TestVolatile {
  2. // 定义一个成员变量
  3. static boolean flag = true;
  4. public static void main(String[] args) {
  5. new Thread(() -> {
  6. System.out.println(Thread.currentThread().getName() + "start");
  7. try {
  8. Thread.sleep(2000);
  9. } catch (InterruptedException e) {
  10. throw new RuntimeException(e);
  11. }
  12. flag = false;
  13. System.out.println(Thread.currentThread().getName() + "end");
  14. }).start();
  15. new Thread(() -> {
  16. System.out.println(Thread.currentThread().getName() + "start");
  17. while (flag) {
  18. // 空转
  19. }
  20. System.out.println(Thread.currentThread().getName() + "end");
  21. }).start();
  22. }
  23. }

3、解决共享变量可见性的两种方式
  • volatile关键字
  • 加锁

3.1、volatile关键字处理,还是上述代码,只要在flag属性前加一个volatile关键字,就可以了。

3.2、 加锁处理,看代码

线程1赋值修改flag,线程2不断读取flag,可以看到flag被线程1修改后,线程2是可以读取到变化之后的结果的。

  1. public class TestVolatile {
  2. // 定义一个成员变量
  3. static boolean flag = true;
  4. static final Object lock = new Object();
  5. public static void main(String[] args) {
  6. test02(lock);
  7. }
  8. private static void test02(Object lock) {
  9. new Thread(() -> {
  10. System.out.println(Thread.currentThread().getName() + "start");
  11. try {
  12. Thread.sleep(2000);
  13. } catch (InterruptedException e) {
  14. throw new RuntimeException(e);
  15. }
  16. flag = false;
  17. System.out.println(Thread.currentThread().getName() + "end");
  18. }).start();
  19. new Thread(() -> {
  20. System.out.println(Thread.currentThread().getName() + "start");
  21. while (true) {
  22. synchronized (lock) {
  23. if (flag) {
  24. System.out.println("flag = " + flag);
  25. try {
  26. Thread.sleep(1000);
  27. } catch (InterruptedException e) {
  28. throw new RuntimeException(e);
  29. }
  30. } else {
  31. System.out.println("flag = " + flag);
  32. break;
  33. }
  34. }
  35. }
  36. System.out.println(Thread.currentThread().getName() + "end");
  37. }).start();
  38. }
  39. }

执行结果

4、解决共享变量可见性的原理?
  1. 加锁:某一个线程进入synchronized代码块后,执行过程如下:线程获得锁,清空工作内存,从主内存中拷贝最新值到工作内存,执行代码,修改后的副本值刷新回主内存,线程释放锁
  2. volatile关键字:其实还是工作内存的及时刷新,volatile有以下语义
    1. 写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
    2. 读一个volatile变量时,JMM会把该线程本地内存设为无效,重新回到主内存中读取最新共享变量。

5、volatile不保证原子性

volatile是不保证原子性操作的。

  1. static volatile int count = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. for (int i = 0; i < 10; i++) {
  4. new Thread(() -> {
  5. for (int j = 0; j < 10000; j++) {
  6. count++;
  7. }
  8. System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
  9. }).start();
  10. }
  11. }

如上述代码,定义了一个volatile修饰的int类型变量,启动10个线程去执行++操作,每个线程修改10000次,按理说修改后的值应该为100000,但是每次执行的结果都没有到100000

发生上述问题的原因在于,count++这个操作不是原子性的,他包含三个步骤:

  1. 从主内存中读取数据导工作内存
  2. 对工作内存中的数据进行+1操作
  3. 将工作内存中的数据写会到主内存

假设某一时间,两个线程都执行到了步骤1,读取到的count值是100 ,然后线程1的CPU时间片到了,停止执行,此时2线程继续执行23步骤,将主内存的值修改为101,这个时候线程1继续执行,但是因为1已经执行了,没有重新去主内存中取值,因此执行23操作后,新值为101,然后往主内存修改的值也是101。

解决原子性办法,

1、加锁

  1. static final Object lock = new Object();
  2. static volatile int count = 0;
  3. public static void main(String[] args) throws InterruptedException {
  4. for (int i = 0; i < 10; i++) {
  5. new Thread(() -> {
  6. for (int j = 0; j < 10000; j++) {
  7. synchronized (lock){
  8. count++;
  9. }
  10. }
  11. System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
  12. }).start();
  13. }
  14. }

2、使用atomic包 // 底层CAS,不多介绍了。

  1. static AtomicInteger a = new AtomicInteger();
  2. public static void main(String[] args) throws InterruptedException {
  3. for (int i = 0; i < 10; i++) {
  4. new Thread(() -> {
  5. for (int j = 0; j < 10000; j++) {
  6. a.getAndIncrement();
  7. }
  8. System.out.println(Thread.currentThread().getName() + "==========> count=" + a);
  9. }).start();
  10. }
  11. }

6、指令重排序介绍。

什么是重排序?为了提高性能,编译器和处理器常常会对指令进行重新排序。一般重排序分为以下三种:编译器优化重排序,指令级并行重排序,内存系统重排序。

重排序就是为了提高处理初度,如下图。

6.1、指令重排序在多线程并发下会产生什么问题?

经典案例,以下代码执行完毕,i 和 j 的值有可能是多少?经过测试,i=0,j=0的情况也会出现!这就是指令重排序导致的问题,

  1. import java.util.concurrent.CountDownLatch;
  2. public class T01_Disorder {
  3. private static int x = 0, y = 0;
  4. private static int a = 0, b = 0;
  5. public static void main(String[] args) throws InterruptedException {
  6. for (long i = 0; i < Long.MAX_VALUE; i++) {
  7. x = 0;
  8. y = 0;
  9. a = 0;
  10. b = 0;
  11. CountDownLatch latch = new CountDownLatch(2);
  12. Thread one = new Thread(new Runnable() {
  13. public void run() {
  14. a = 1;
  15. x = b;
  16. latch.countDown();
  17. }
  18. });
  19. Thread other = new Thread(new Runnable() {
  20. public void run() {
  21. b = 1;
  22. y = a;
  23. latch.countDown();
  24. }
  25. });
  26. one.start();
  27. other.start();
  28. latch.await();
  29. String result = "第" + i + "次 (" + x + "," + y + ")";
  30. if (x == 0 && y == 0) {
  31. System.err.println(result);
  32. break;
  33. }
  34. }
  35. }
  36. }

7、volatile是怎么保证指令执行顺序的?

jvm级别,识别到volatile关键词,会执行jvm内存屏障,包括 loadload 屏障、storestore 屏障、loadstore屏障、storeload 屏障(其中load是读,store是写);

a) 会在写之前加 storestore,写之后加storeload,保证在自己写之前完成其他的写,在自己写完之后才能继续其他的读

b) 会在读之后加上loadload 和 loadstore ,保证在自己读完之后其他的才能读,自己读完之后,其他的才能写

8、 Happens Before原则?

简单的说,如果 A    Happens Before B ,那么,A的操作对B,都是可见的。

 Happens Before模型是由8条具体规则组成的:

  1. 程序顺序规则:单线程中,每个操作 都  Happens Before 他后面的操作。
  2. 监视器规则:一个线程解锁, Happens Before 后面线程的加锁
  3. volatile变量规则:对一个volatile变量的写, Happens Before 对这个volatile的读
  4. 传递规则:A  Happens Before B,B  Happens Before C,则 A  Happens Before C
  5. start() 规则:如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作 happens-before  线程B的任意操作。
  6. join() 规则:如果线程A执行ThreadB.join(),那么B线程中的任意操作 happens-before 线程A从ThreadB.join()成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用 happens-before 被中断线程的代码检测到中断时间的发生;
  8. 对象finalize规则:一个对象初始化完成(构造函数执行结束)happens-before 于发生它的finalize()方法的开始

9、总结一下volatile关键字的作用
  1. 保证变量的可见性
  2. 禁止指令重排序

10、volatile和synchronized的区别?
  1. 关键字使用范围:volatile只能修饰变量,synchronized关键字能修饰变量,方法,代码块
  2. 是否会阻塞线程:volatile不会阻塞线程,synchronized会阻塞线程
  3. 原子性:volatile不保证原子性,synchronized可以保证原子性
  4. 可见性:volatile 和 synchronized 都可以保证 修改的可见性
  5. 指令重排序:volatile禁止指令重排序,synchronized允许被编译器优化。

总的来说,volatile的本质是告诉JVM,变量在工作内存(寄存器)中的值是不确定的,需要从主存中去取,synchronized则是直接锁住当前变量,只有当前线程可以访问,其他线程阻塞。

11、单例模式中,使用了双重判断,为什么还需要volatile关键字?

synchronized关键字可以保证原子性和可见性,但是没有办法保证顺序性(即可以被指令重排序),new 一个对象可以分为以下三个步骤:

  1. 为对象分配内存空间
  2. 初始化对象
  3. 对象地址的引用

假设new 对象的过程发生了指令重排序,步骤2和3互换。虽然创建对象加锁了,但是加锁线程1执行完1和3之后失去了CPU时间片,此时初始化对象还没有完成,这个时候线程2执行,在外层判断的时候INSATNCE!=null, 直接就获取对象执行操作了,但是因为该对象实际还没有初始化完成呢,因此线程2返回的就是一个空对象。

而volatile关键字禁止指令重排序,就避免了上述问题。

  1. public static T01 getInstance(){
  2. private static volatile T01 INSTANCE;
  3. private T01 (){
  4. //私有构造器,外部不能new
  5. }
  6. if(INSTANCE==null){
  7. synchronized (T01.class){
  8. if(INSTANCE==null){
  9. INSTANCE=new T01();
  10. }
  11. }
  12. }
  13. return INSTANCE;
  14. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/286427
推荐阅读
相关标签
  

闽ICP备14008679号