volatile的使用以及引起的问题解决方法

Posted on 2019-09-20 09:53  MrEven  阅读(1613)  评论(0编辑  收藏  举报

volatile是什么?有什么特性?

1、volatile是java虚拟机提供的轻量级的同步机制

三大特性如下:

1、保证可见性 

2、不保证原子性

3、禁止指令重排

JMM内存模型是如何实现可见性?

JVM运行程序实体是线程,线程创建时JVM都会为其开辟一个工作内存,工作内存是每个线程私有的数据区域,而java内存模型中规定所有变量都存在主内存中 (主内存:相当于电脑上的内存条,有8G、16G等...)线程对变量的操作(读取和赋值)必须在自己的工作内存中进行,

首先要将变量从主内存中拷贝到自己的工作内存空间,然后对变量进行操作,操作完后再将变量写会主内存。  如下图:

 

                                              

                                                     图1                                                                                                                            图2

1、由于添加了volatile的缘故,线程A对主内存共享变量的操作线程B是可见的,这就体现了volatile三大特性之一的 “保证可见性”

2、不保证原子性的原因:假如主内存有一个变量值为 age=18,  线程A和线程B分别将主内存的变量拷贝到自己的工作内存中, 线程A将age的值修改成 20,

然后在写回主内存,此时主内存的值为 20;由于各种原因,线程B网络故障导致修改时间比线程A慢了十几秒;而此时线程A又把主内存中的 20 改成 18,这时线程B

通过CAS(ComPareAndSwap):比较并交换)发现主内存中的值还是 18;于是把 18 改成了 22,虽然修改成功了,但并不表示没有问题了;CAS操作引起了ABA的问题。

3、禁止指令重排:多线程环境下线程交替执行,由于编译器优先重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,用volatile修饰变量

可防止指令重排

 验证volatile可见性、解决volatile不保证原子性的case

package com.company;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyData {
    volatile int number = 0;//没加volatile的时候不保证可见性

    public void addTo() {
        this.number = 60;
    }

    //此時已經添加了volatile,不保证原子性
    public void addNumber() {
        number++;
    }

    //AtomicInteger解决原子性问题
    AtomicInteger atomicInteger = new AtomicInteger();//不传参数,底层源码默认是 0

    public void addPlus() {
        atomicInteger.getAndIncrement();//Atomically increments by one the current value.   源码注释每调一次就会自增 1 
    }
}

/**
 * 1.0  验证 volatile 的可见性
 * 1.1  假设int number = 0,number变量不添加volatile关键字修饰,没有可见性
 * 1.2  添加了volatile,可以解决可见性问题
 * <p>
 * 2.0  volatile 不保证原子性
 */
public class VolatileDemo {
    public static void main(String[] args) {
        atomicIntegerSee();
        SeeVolatile();
    }

    /**
     * AtomicInteger保证原子性
     */
    public static void atomicIntegerSee() {
        MyData myData = new MyData();
        /**
         * 验证volatile不保证原子性
         * number添加了volatile不保证原子性
         * 20加到1000,答案是20000;但volatile不保证原子性
         * 如何解决volatile不保证原子性问题?
         *   方法一:在方法上面添加 synchronized 关键字修饰,但每次访问只能有一个线程;导致并发性下降
         *   方法二:使用 AtomicInteger  即可以保证原子性,又不会降低并发量
         */
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addNumber();//volatile不保证原子性,所以最终结果不会是20000;会比20000小;有可能侥幸是20000
                    myData.addPlus();// 20个线程怎么加都是20000.能解决原子性的问题
                }
            }, String.valueOf(i)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t volatile number value is: " + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger number value is: " + myData.atomicInteger);
    }

    /**
     * 1.0  验证volatile的可见性
     */
    public static void SeeVolatile() {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }//线程休眠3秒钟
            myData.addTo();//这时候number已经变成60,但由于没有添加volition关键字,其他线程并不知道number已经变成60
            System.out.println(Thread.currentThread().getName() + "\t updata number:  " + myData.number);//此时拿到的number已经是修改完后写回主内存的number
        }, "AAA").start();

        while (myData.number == 0) {
            //如果取到的number等于0,main线程就会执行里面的内容;但此时main线程已经等于60;所以线程一直处于等待状态,下面的语句无法输出
        }
        System.out.println(Thread.currentThread().getName() + "\t main Thread number is fish,main get number: " + myData.number);
    }
}
View Code

 解决CAS引起的ABA问题

 1 package com.company;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 import java.util.concurrent.atomic.AtomicReference;
 5 import java.util.concurrent.atomic.AtomicStampedReference;
 6 
 7 /**
 8  * 版本号的原子引用
 9  * 解决ABA问题:AtomicStampedReference
10  */
11 public class ABADemo {
12     static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
13     static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(100, 1);
14 
15     public static void main(String[] args) {
16         System.out.println("=================================以下是ABA问题的产生=======================================");
17         new Thread(() -> {
18             atomicReference.compareAndSet(100, 101);
19             atomicReference.compareAndSet(101, 100);
20         }, "t1").start();
21         new Thread(() -> {
22             try {
23                 TimeUnit.SECONDS.sleep(1);
24             } catch (InterruptedException e) {
25                 e.printStackTrace();
26             }
27             System.out.println(atomicReference.compareAndSet(100, 2019) + "\t " + atomicReference.get());//修改成功,但t1中间修改了两次又改回100,所以存在ABA问题
28         }, "t2").start();
29         try {
30             TimeUnit.SECONDS.sleep(2);
31         } catch (InterruptedException e) {
32             e.printStackTrace();
33         }//暂停两秒钟,保证上面的t1和t2 线程都操作完
34         System.out.println("=================================以下是ABA问题的解决=======================================");
35         new Thread(() -> {
36             int stamp = atomicStampedReference.getStamp();//获取当前版本号
37             try {
38                 TimeUnit.SECONDS.sleep(1);
39             } catch (InterruptedException e) {
40                 e.printStackTrace();
41             }//休眠1秒,让t4也获得当前版本号
42             System.out.println(Thread.currentThread().getName() + "\t 第一版本号为:" + stamp);
43             atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
44             System.out.println(Thread.currentThread().getName() + "\t 第二版本号为:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference());
45             atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
46             System.out.println(Thread.currentThread().getName() + "\t 第三版本号为:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference());
47         }, "t3").start();
48         new Thread(() -> {
49             int stamp = atomicStampedReference.getStamp();//获取当前版本号
50             System.out.println(Thread.currentThread().getName() + "\t 第一版本号:" + stamp);
51             try {
52                 TimeUnit.SECONDS.sleep(3);
53             } catch (InterruptedException e) {
54                 e.printStackTrace();
55             }//休眠3秒,让t3完成一次ABA的操作
56             boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);//修改失败,版本号不匹配
57             /*boolean result=atomicStampedReference.compareAndSet(100,2019,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);*/
58             System.out.println(Thread.currentThread().getName() + "\t 是否修改成功:" + result + "\t 当前最新版本为:" + atomicStampedReference.getStamp());
59             System.out.println(Thread.currentThread().getName() + "\t 当前主内存的最新值为:" + atomicStampedReference.getReference());
60         }, "t4").start();
61     }
62 }
View Code

 

 CAS优缺点

优点:

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

缺点:

1、ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A -> B -> A的过程。

2、循环时间长开销大。如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销。

3、只能对一个变量进原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。

Copyright © 2024 MrEven
Powered by .NET 8.0 on Kubernetes