volatile关键字的作用:保证变量的可见性

一定要谨记 volatile 关键字在 Java 代码中仅仅保证这个变量是可见的:它不保证原子性。在那些非原子且可由多个线程访问的易变操作中,一定不能够依赖于 volatile 的同步机制。相反,要使用 java.util.concurrent 包的同步语句、锁类和原子类。它们在设计上能够保证程序是线程安全的。

volatile作用的具体描述

Java如何保证可见性

Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

                                                                                            引自- wangzzu的博客文章

上面这段话能十分清楚的说明这个变量的用处了吧。下面配合代码更加准确的验证这个问题。

 代码引自-- 阿里工程师oldratlee的github

Demo类com.oldratlee.fucking.concurrency.NoPublishDemo



Demo说明

主线程中设置属性stoptrue,以控制在main启动的任务线程退出。



问题说明

在主线程属性stoptrue后,但任务线程持续运行,即任务线程中一直没有读到新值。

出现上述问题说明的代码:

public class NoPublishDemo {
    boolean stop = false;

    public static void main(String[] args) {
        // LoadMaker.makeLoad();

        NoPublishDemo demo = new NoPublishDemo();

        Thread thread = new Thread(demo.getConcurrencyCheckTask());
        thread.start();

        Utils.sleep(1000);
        System.out.println("Set stop to true in main!");
        demo.stop = true;
        System.out.println("Exit main.");
    }

    ConcurrencyCheckTask getConcurrencyCheckTask() {
        return new ConcurrencyCheckTask();
    }

    private class ConcurrencyCheckTask implements Runnable {
        @Override
        public void run() {
            System.out.println("ConcurrencyCheckTask started!");
            // 如果主线中stop的值可见,则循环会退出。
            // 在我的开发机上,几乎必现循环不退出!(简单安全的解法:在running属性上加上volatile)
            while (!stop) {
            }
            System.out.println("ConcurrencyCheckTask stopped!");
        }
    }
}

例子中主线程的stop变量没有做任何的显示定义volatile。

当我们执行后的打印结果为:

ConcurrencyCheckTask started!
Set stop to true in main!
Exit main.

并没有打印



ConcurrencyCheckTask stopped!



并且程序还没有结束。

接下来我们对stop变量添加volatile



volatile boolean stop = false;



程序完美结束。也印证了上文描述中的内容。

组合状态读到无效组合

--此时使用volatile则无法解决同时修改两个值,数据还是返回正确的问题。

意思是同时对两个值修改,但是实际返回确实错误的。

程序设计时,会需要多个状态记录(状态可以是个POJO对象或是int等等)。常看到多状态读写没有同步的代码,并且写的同学会很自然地就忽略了线程安全的问题。

无效组合 是指 从来没有设置过的组合。



Demo说明

主线程修改多个状态,为了方便检查,每次写入有个固定的关系:第2个状态是第1个状态值的2倍。在任务线程中读取多个状态。
Demo类com.oldratlee.fucking.concurrency.InvalidCombinationStateDemo



问题说明

任务线程中读到了 第2个状态不是第1个状态值2倍的值,即是无效值。



快速运行



mvn compile exec:java -Dexec.mainClass=com.oldratlee.fucking.concurrency.InvalidCombinationStateDemo



实际测试代码

public class InvalidCombinationStateDemo {
    public static void main(String[] args) {
        CombinationStatTask task = new CombinationStatTask();
        Thread thread = new Thread(task);
        thread.start();

        Random random = new Random();
        while (true) {
            int rand = random.nextInt(1000);
            task.state1 = rand;
            task.state2 = rand * 2;
        }
    }

    private static class CombinationStatTask implements Runnable {
        // 对于组合状态,加 volatile 不能解决问题
        volatile int state1;
        volatile int state2;

        @Override
        public void run() {
            int c = 0;
            for (long i = 0; ; i++) {
                int i1 = state1;
                int i2 = state2;
                if (i1 * 2 != i2) {
                    c++;
                    System.err.printf("Fuck! Got invalid CombinationStat!! check time=%s, happen time=%s(%s%%), count value=%s|%s\n",
                            i + 1, c, (float) c / (i + 1) * 100, i1, i2);
                } else {
                    // 如果去掉这个输出,则在我的开发机上,发生无效组合的概率由 ~5% 降到 ~0.1%
                    System.out.printf("Emm... %s|%s\n", i1, i2);
                }
            }
        }
    }

}



long变量读到无效值

无效值 是指 从来没有设置过的值。

long变量读写不是原子的,会分为2次4字节操作。
Demo类com.oldratlee.fucking.concurrency.InvalidLongDemo



Demo说明

主线程修改long变量,为了方便检查,每次写入的long值的高4字节和低4字节是一样的。在任务线程中读取long变量。



问题说明

任务线程中读到了高4字节和低4字节不一样的long变量,即是无效值。



快速运行



mvn compile exec:java -Dexec.mainClass=com.oldratlee.fucking.concurrency.InvalidLongDemo



DEMO:

public class InvalidLongDemo {
    long count = 0;

    public static void main(String[] args) {
        // LoadMaker.makeLoad();

        InvalidLongDemo demo = new InvalidLongDemo();

        Thread thread = new Thread(demo.getConcurrencyCheckTask());
        thread.start();

        for (int i = 0; ; i++) {
            long l = i;
            demo.count = l << 32 | l;
        }
    }

    ConcurrencyCheckTask getConcurrencyCheckTask() {
        return new ConcurrencyCheckTask();
    }

    private class ConcurrencyCheckTask implements Runnable {
        @Override
        public void run() {
            int c = 0;
            for (int i = 0; ; i++) {
                long l = count;
                long high = l >>> 32;
                long low = l & 0xFFFFFFFFL;
                if (high != low) {
                    c++;
                    System.err.printf("Fuck! Got invalid long!! check time=%s, happen time=%s(%s%%), count value=%s|%s\n",
                            i + 1, c, (float) c / (i + 1) * 100, high, low);
                } else {
                    // 如果去掉这个输出,则在我的开发机上没有观察到invalid long
                    System.out.printf("Emm... %s|%s\n", high, low);
                }
            }
        }
    }

}

对于单个变量的值修改 给变量添加volatile也是可以解决此问题的。