美文网首页
volatile关键字

volatile关键字

作者: 不知名的蛋挞 | 来源:发表于2020-02-19 17:52 被阅读0次

    volatile关键字保证可见性

    首先,我们先看一段小程序。

    public class VolatileDemo {
    
        boolean running = true;
    
        public static void main(String[] args){
            VolatileDemo demo = new VolatileDemo();
            new Thread(demo::test,"t1").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            demo.running = false;
        }
    
        public void test(){
            System.out.print("test start....");
            while (running){
    
            }
            System.out.print("test end....");
        }
    }
    

    程序运行结果:

    "test start...."
    

    并且程序不会停止。

    按理来说,应该会在main线程修改running值后,主线程会打印出"test end....",但是为什么没有出现预期效果呢?下面来分析这段程序,涉及到内存可见性问题。

    Java内存模型中,有一个主内存,主要是存放线程共享的东西(比如堆、静态变量)。当程序运行时,JVM会为每一个执行任务的线程分配一个独立的缓存空间,用于存放内存私有的东西(如局部变量),这个就是工作内存。

    显然,running是一个公共的变量,所以存在于主内存中。

    工作内存是一块连续的空间,所以在里面找东西是比较快的。但是主内存不一样,主内存比较大,并且空间是不连续的。如果每次线程执行的时候都要去主内存找running值就会很麻烦,所以线程在执行的时候,会把在主内存中的running复制一份放到自己的工作内存中,这个就是running的副本。

    不难理解,程序开始执行时,t1线程和main线程获取到的running都为true。但为什么main线程将running改为false后,线程t1没有打印出“test end.....”还是继续在跑呢?原因在于:while(true)执行效率很高,使得线程t1没有时间再次从主存中获取flag的值,因此程序在main线程将running修改为false后,没有停止运行的原因。其实在while(true)后面稍微延迟一点(比如说,打印一句话),都会使线程t1有时间去主存中读取running=false

     public void test(){
            System.out.print("test start....");
            while (running){
                System.out.print("doing sth");
            }
            System.out.print("test end....");
        }
    

    产生这种情况的原因就在于,两个线程在操作共享数据时,对共享数据的操作是彼此不可见的。如果主存的变量值改变了,那么就必须要写回到线程工作内存中的,线程才能感知。

    那么为了不让这种问题出现,怎么解决呢?可以给running加上volatile关键字:

    volatile boolean running = true;
    

    当多个线程进行操作共享数据时,可以保证内存中的数据可见。底层原理:内存栅栏。使用volatile关键字修饰时,可理解为对数据的操作都在主存中进行。

    volatile关键字不保证原子性

    我们再来看下面一段程序:

    public class VolatileAndSynDemo {
    
        volatile int count = 0;
    
        public static void main(String[] args){
            VolatileAndSynDemo demo = new VolatileAndSynDemo();
    
            List<Thread> threads = new ArrayList<>();
            for(int i=0;i<10;i++){
                threads.add(new Thread(demo::test,"thread-"+i));
            }
    
            threads.forEach((o)->o.start());
    
            threads.forEach((o)->{
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            
            System.out.print(demo.count);
        }
    
        public void test(){
            for(int i=0;i<10000;i++){
                count++;
            }
        }
    }
    

    输出结果:每次执行结果都不一样,反正到不了100000。

    原因就是因为:假设有3个线程去操作count(调用test()方法执行10000次),此时count=100。虽然说volatile保证了count的可见性。但是如果线程t1和线程t2同时读取count,那么两个线程拿到的count都是100然后第一个线程往回写,count=101;第二个线程是不会管此时count=101的,往回写还是101。造成了重复写的情况,所以最终得到的值永远不会是100000。

    所以volatile并不能保证原子性。为了保证count的原子性,我们可以给test()方法上锁:加上synchronized关键字,当然这只是其中一种方法。这样synchronized既保证原子性也保证了可见性。

    public synchronized void test(){
            for(int i=0;i<10000;i++){
                count++;
            }
        }
    

    所以volatile尽量别用,除非要禁止重排序!!!

    相关文章

      网友评论

          本文标题:volatile关键字

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