Volatile 关键字

目录

volatile 关键字

1 可见性的测试

1)单线程出现死循环 

2)使用多线程解决死循环 

3)使用多线程有可能出现死循环 

4)使用 volatile 关键字解决多线程出现的死循环 

5)synchronized代码块也具有增加可见性的作用 

2 原子性与非原子性的测试

1) 32位JDK中long或double数据类型写操作为非原子性

2)使用volatile解决在32位JDK中long 或double数据类型写操作为非原子性的问题

3)volatile int i ++ 操作是非原子性的

4)使用 atomic 原子类进行 i ++操作实现原理

5)逻辑混乱与解决方案 

简单介绍一下CAS:

3 禁止代码重排序的测试 

1)实现代码重排序的测试

2)volatile关键字之前的代码可以重排序 

3)volatile 关键字之后的代码可以重排序 

4)volatile关键字之前的代码不可以重排到volatile之后 

5)volatile 关键字之后的代码不可以重排到volatile之前 

6)synchronized关键字之前的代码不可以重排到synchronized之后

7)synchronized关键字之后的代码不可以重排到synchronized之前 

8)总结 


volatile 关键字

在Java中 volatile 关键字就像一个神话一样,几乎在各种博客,微信订阅号,聊天群被反复谈起,可见程序员对此是又爱又恨,也说明volatile在多线程领域的重要性。volatile 在使用上有 以下 3 个特性。

1)可见性: B线程能马上看到A线程更改的数据。

2)原子性: 原子性是指一组操作在执行时 不能被打断。如果在中间执行其他操作会导致者一组操作不连续,获得错误的结果,即非原子性。

      volatile的原子性体现在赋值原子性,在 32 位 JDK中对 64 位数据类型执行赋值操作时会写两次,高32位 和 低 32 位 ,如果写两次的这组操作被打断,导致写入的数据被其他线程的写操作所覆盖,会获得错误的结果,就是非原子性的。

       在 32位 JDK中针对 未使用 volatile 声明 的 long 或 double 的 64 位数据类型没有实现写 原子性,如果想实现,需要在声明变量时添加volatile,而在64位JDK中,是否具有原子性取决于具体的实现,在X86架构64位JDK版本中,写double 或 long 是原子性的 。针对 volatile 声明的 int i 变量进行 i++操作时是非原子性的。

3)禁止代码重排序

1 可见性的测试

volatile 关键字具有可见性,可以提高软件的灵敏度。测试代码如下:

1)单线程出现死循环 

public class t99 {
    static class PrintString{
        private boolean isContinuePrint = true;
        public  boolean isContinuePrint(){
            return isContinuePrint;
        }

        public void setContinuePrint(boolean continuePrint) {
            isContinuePrint = continuePrint;
        }
        public void printStringMethod(){
            try {
                while(isContinuePrint){
                    System.out.println("run printStringMethod threadName="+Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrintString printString = new PrintString();
        printString.printStringMethod();
        System.out.println("我要停止它 !stopThread="+Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

 程序在运行后根本停不下来,结果如图:

停不下来的主要原因是main线程一直在处理while循环,导致程序不能执行后面的代码,解决的办法当然是使用多线程技术。

2)使用多线程解决死循环 

public class t99 {
    static class PrintString implements Runnable{
        private boolean isContinuePrint = true;
        public  boolean isContinuePrint(){
            return isContinuePrint;
        }

        public void setContinuePrint(boolean continuePrint) {
            isContinuePrint = continuePrint;
        }
        public void printStringMethod(){
            try {
                while(isContinuePrint){
                    System.out.println("run printStringMethod threadName="+Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            printStringMethod();
        }
    }

    public static void main(String[] args) {
        PrintString printString = new PrintString();
        new Thread(printString).start();
        System.out.println("我要停止它 !stopThread="+Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

运行结果如图: 

3)使用多线程有可能出现死循环 

注意:while语句中一定要执行空循环,即使是System.out.println()也不行,因为System.out.println()源码中含有synchronized 关键字增加可见性。 

public class t16 {
    static class RunThread extends Thread{
        private boolean isRuning = true;
        public boolean isRuning(){
            return isRuning;
        }
        public void  setRuning(boolean isRuning){
            this.isRuning = isRuning;
        }

        @Override
        public void run() {
            System.out.println("进入 run 了");
            while(isRuning){
            }
            System.out.println(" 线程被停止了");
        }
    }

    public static void main(String[] args) {
        try{
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRuning(false);
            System.out.println("已经赋值为false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

程序结果可能出现死循环,也可能不出现死循环。如果不出现死循环,可以在JVM中将当前运行的模式设置为服务器模式,配置的步骤是在idea 中对JVM添加运行参数 -server,代表更改运行的模式为服务器模式,设置参数:

运行后出现了死循环: 

 

代码System.out.println(" 线程被停止了")永远不会被执行。 

4)使用 volatile 关键字解决多线程出现的死循环 

是什么原因导致的死循环呢?在启动线程时,因为变量private boolean isRuning = true;分别存储在公共内存线程的私有内存中,线程运行后在线程的私有内存中取得isRuning的值一直是true,而代码"thread.setRuning(false);"虽然被执行,却是将公共内存中的 isRuning变量改成false,操作的是两块内存地址中的数据,所以一直处于死循环的状态,内存结构如图:

这个问题其实就是私有内存中的值和公共内存中的值不同步造成的,可以通过volatile关键字来解决 ,volatile的主要作用就是当线程访问isRuning变量时,强制的从公共内存中取值。

修改RunThread类如下:

static class RunThread extends Thread{
        volatile private boolean isRuning = true;
        public boolean isRuning(){
            return isRuning;
        }
        public void  setRuning(boolean isRuning){
            this.isRuning = isRuning;
        }

        @Override
        public void run() {
            System.out.println("进入 run 了");
            while(isRuning){
              
            }
            System.out.println(" 线程被停止了");
        }
    }

运行结果如图: 

线程终于被正确的停止了!这种方式就是1.11节介绍的第一种停止线程的方法:使用退出标志使线程正常退出。 

通过使用volatile关键字,强制地从公共内存中读取变量的值再同步线程的私有内存中,结构如图所示:

使用volatile关键字是增加了实例变量在多个线程之间的可见性。 

5)synchronized代码块也具有增加可见性的作用 

synchronized关键字可以使多个线程访问同一个资源,具有同步性,也可以使线程私有内存中的变量与公共内存中的变量同步,也就是可见性。

实验如下: 

public class synchronizedUpdateNewValue {
    static class Service{
        private boolean isContinueRun = true;
        public void runMethod(){
            while(isContinueRun){
            }
            System.out.println("停下来了!");
        }
        public void stopMethod(){
            isContinueRun= false;
        }
    }
    static class ThreadA extends Thread{
        private Service service;

        public ThreadA(Service service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            service.runMethod();
        }
    }
    static class ThreadB extends Thread{
        private Service service;

        public ThreadB(Service service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            service.stopMethod();
        }
    }

    public static void main(String[] args) {
        try{
            Service service = new Service();
            ThreadA threadA = new ThreadA(service);
            threadA.start();
            Thread.sleep(1000);
            ThreadB threadB =new ThreadB(service);
            threadB.start();
            System.out.println("已经发起了停止的命令");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

 运行后出现死循环:

这个结果是因各线程间的数据值没有可见性造成的,而synchronized关键字可以使数据具有可见性,更改Service类如下:

static class Service{
        private boolean isContinueRun = true;
        public void runMethod(){
            String anyString = new String();
            while(isContinueRun){
                synchronized (anyString){
                }
            }
            System.out.println("停下来了!");
        }
        public void stopMethod(){
            isContinueRun= false;
        }
    }

再次运行,结果正常退出:

synchronized关键字会把私有内存中的数据同公共内存同步,使私有内存中的数据和公共内存中的数据一致

2 原子性与非原子性的测试

32位JDK中针对未使用volatile声明的longdouble64位数据类型没有实现赋值写的原子性,如果想实现,声明变量时添加volatile。如果在64位JDK中,是否原子取决于具体实现,在X86架构的64位JDK版本中,写double和long是原子的。

另外volatile关键字最致命的缺点是不支持运算原子性,也就是多个线程对用volatile修饰的变量 i 执行 i--/i++ 操作时,i--/i++ 操作还是会被分解成三步,造成非线程安全问题

1) 32位JDK中long或double数据类型写操作为非原子性

验证之前先确认环境是否为32位的JDK ,可以使用查看:

java -version

 例如本人的:

可以看到是 64Bit 也就是64位 ,需要专门准备一个32Bit的测试环境。  

案例测试: 

public class long_double_32_noATOMIC_test {
    static class MyService {
        public long i;
    }

    static class ThreadA extends Thread {
        private MyService service;

        public ThreadA(MyService service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            while (true) {
                service.i = 1;
            }
        }
    }

    static class ThreadB extends Thread {
        private MyService service;

        public ThreadB(MyService service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            while (true) {
                service.i = -1;
            }
        }
    }

    public static void main(String[] args) {
        try {
            MyService service = new MyService();
            ThreadA a = new ThreadA(service);
            ThreadB b = new ThreadB(service);
            a.start();
            b.start();
            Thread.sleep(1000);
            System.out.println("long 1 二进制值是:" + Long.toBinaryString(1));
            System.out.println("long -1 二进制值是:" + Long.toBinaryString(-1));
            while (true) {
                long getValue = service.i;
                if (getValue != 1 && getValue != -1) {
                    System.out.println("            i的值是:" + Long.toBinaryString(getValue) + " 十进制是:" + getValue);
                    System.exit(0);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

32Bit运行结果如图:

程序运行后变量 i 的值即不是 1 也不是 -1 , 是 -4294967295 , 说明在32位JDK中对 long 或double 数据类型进行写操作是非原子的。

也有一定比率出现 4294967295的结果,4294967295的二进制数值为 32位 的 1组成。

2)使用volatile解决在32位JDK中long 或double数据类型写操作为非原子性的问题

更改业务代码如下:

 static class MyService {
       volatile public long i;
    }

在32位JDK中,在long 或 double 数据类型的变量前加入volatile关键字后再进行写操作是原子性的,程序再次运行后控制台并未输出信息,说明 i 的 值不是 1 就是 -1。

32 位JDK中对 volatile 的 long 或 double 变量进行赋值是非原子性的,所以需要添加volatile关键字。如果在64位JDK中进行写操作是原子性的,则不需要添加 volatile 关键字。 

3)volatile int i ++ 操作是非原子性的

这是因为 i++ 操作不是原子性的,会拆分成 3 个步骤,此知识点在本人其他文章有过介绍。 

volatile 关键字不支持 i ++ 运算原子性,使用多线程执行 volatile int i ++ 赋值操作是非原子性的, i--操作的行为也是一样,下面进行测试。 

public class volatileTest {
    static class MyThread extends Thread{
        volatile public static int count;
        private static void addCount(){
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count="+count);
        }

        @Override
        public void run() {
            addCount();
        }
    }

    public static void main(String[] args) {
        MyThread[] MyThreadArrary = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            MyThreadArrary[i] = new MyThread();
        }
        for (int i = 0; i < 100; i++) {
            MyThreadArrary[i].start();
        }
    }
}

运行结果:

运行结果不是10000,说明在多线程环境下 volatile  public static int count ++ 运算操作是非原子性的。

更改自定义线程代码如下:

static class MyThread extends Thread{
        volatile public static int count;

        /**
         * 一定添加static关键字,
         * 这样synchronized 与 static锁的内用就是 MyThread.class类。
         * 也就达到了同步所有实例的效果。
         */
        synchronized private static void addCount(){
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count="+count);
        }

        @Override
        public void run() {
            addCount();
        }
    }

程序运行结果如图:

在本次示例中,如果在方法前加入 synchronized 同步关键字,就没有必要再使用volatile关键字来声明变量了。

volatile 关键字主要是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用。例如:在32位JDK中增加赋值操作的原子性。

volatile 关键字提示线程每次从公共内存中取读取变量,而不是私有内存中去读取,这样就保证了同步数据的可见性。但需要注意的是:如果修改实例变量中的数据,比如 i ++,则这样的操作其实并不是一个原子操作,也就是非线程安全的。 

当多个线程同时修改时,解决办法是使用synchronized 关键字保证原子性。所以,volatile本身并不处理 int i ++ 运算操作的原子性。

总结:volatile 保证数据在线程之间可见性,但并不保证同步性,同时在 32 位 的JDK中保证赋值操作的原子性 。

4)使用 atomic 原子类进行 i ++操作实现原理

除了再 i++ 操作时使用synchronized 关键字实现同步外,还可以使用 AtomicInteger原子类实现。

原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。

创建新的测试项目:

public class AtomicIntegerTest {
    static class AddCountThred extends Thread{
        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(count.incrementAndGet());//获取值并且+1
            }
        }
    }

    public static void main(String[] args) {
        AddCountThred countService = new AddCountThred();
        Thread t1 = new Thread(countService);
        t1.start();

        Thread t2 = new Thread(countService);
        t2.start();

        Thread t3 = new Thread(countService);
        t3.start();

        Thread t4 = new Thread(countService);
        t4.start();

        Thread t5 = new Thread(countService);
        t5.start();
    }
}

运行结果:

发现成功累加到了50000,但是顺序好像有些混乱。 

5)逻辑混乱与解决方案 

即使在有逻辑的情况下,原子类的输出结果也具有随机性,这是没办法的事,因为JAVA中的原子操作的大部分原理都是采用的CAS(Compare And Swap),是一种只要不成功就不断重试的操作,所以有可能后面的先比前面的尝试成功。 

展示随机性:

public class atomicIntegerNoSafe {
    static class MyService{
        public static AtomicLong aiRef = new AtomicLong();
        public void adNum(){
            System.out.println(Thread.currentThread().getName()+" 加了100之后的值是:"+aiRef.addAndGet(100));
            aiRef.addAndGet(1);
        }
    }
    static class MyThread extends Thread{
        private MyService service;

        public MyThread(MyService service) {
            super();
            this.service = service;
        }

        @Override
        public void run() {
            service.adNum();
        }
    }

    public static void main(String[] args) {
        try{
            MyService service = new MyService();
            MyThread[] array = new MyThread[5];
            for (int i = 0; i < array.length; i++) {
                array[i] = new MyThread(service);
            }
            for (int i = 0; i < array.length; i++) {
                array[i].start();
            }
            Thread.sleep(1000);
            System.out.println(service.aiRef.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果如图:

(JDK1.8环境测试,如果是JDK17,21应该是位数全部都为0,不会出现1,但总结果正确)

JDK17,21:

输出顺序出现了错误,应该每次加一次100再加一次1,出现 这种情况的原因是addAndGet()方法是原子的,但方法之间的调用却是非原子的,此时可以使用同步解决问题。

static class MyService{
        public static AtomicLong aiRef = new AtomicLong();
        synchronized public void adNum(){
            System.out.println(Thread.currentThread().getName()+" 加了100之后的值是:"+aiRef.addAndGet(100));
            aiRef.addAndGet(1);
        }
    }

运行结果(本次JDK1.8和JDK21结果一致):

从运行结果可以看到,输出信息依次加上100再加1,是我们想要的正确的计算过程,累加的顺序是正确的,且结果也是正确的。

简单介绍一下CAS

CAS,比较并交换(Compare And Swap),是一种用于实现多线程并发操作的基本原理。它通常用于确保对共享数据的原子性操作,以避免竞态条件(Race Condition)和保持数据一致性。

CAS 操作包括以下几个步骤:

比较:首先,CAS 操作会比较当前共享变量的值与预期值是否相等。

交换:如果比较结果相等,就会尝试将共享变量的值替换为新的值。

返回结果:CAS 操作会返回替换是否成功的结果。如果替换成功,表示操作成功;如果替换失败,表示有其他线程在操作这个变量,需要重新尝试。

总的来说就是更新新值之前先看与自己预期值是否相等,如果相等就更新,不相等就重试。

3 禁止代码重排序的测试 

volatile关键字可以禁止代码重排序

什么是重排序? 在Java运行时,JIT(Just-In-Time Compiler ,即时编译器)  为了优化程序的运行,可以动态的改变程序代码的运行顺序。比如有如下代码:

这样做的主要原因是在CPU流水线中这 4 个指令是同时执行的,轻耗时的代码在很大程度上会先执行完 ,以让出CPU流水线资源供其他指令使用,所以代码重排是为了追求更高的程序运行效率。

重排序发生在没有依赖关系时,比如上面图示中的 A,B,C,D代码,BCD不依赖 A的结果,CD不依赖AB的结束,D不依赖ABC的结果时就会发生重排序,如果有依赖则代码不会重排序。

而volatile关键字可以禁止代码重排序,比如有如下代码:

那么这会有 4 种情况发生:

1)

AB可以重排序;

2)

CD可以重排序;

3)

AB不可以重排到Z的后面
4)CD不可以重排到Z的前面

也就是说,变量Z是一道屏障,是一堵墙,Z变量之前或之后的代码不可以跨过Z变量。synchronized关键字也具有同样的特性。

1)实现代码重排序的测试

虽然代码重排序之后能提升运行效率,但在有逻辑的程序里很容易出现一些错误。

验证: 

public class Test1 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long c = 0;
    private static long d = 0;
    private static long e = 0;
    private static long f = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count ++;
            Thread t1 = new Thread(() -> {
                a = 1;
                c = 101;
                d = 102;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                e = 201;
                f = 202;
                y = a;
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count+" " + x + "," + y +"";
            if (x == 0 && y == 0){
                System.err.println(showString);
                break;
            }else {
                System.out.println(showString);
            }
        }

    }
}

运行结果如图(很漫长):

程序输出x和y都是0,这时就出现了代码重排序,重排序后的顺序为:

t1t2

x = b;

y = a;

a = 1;

b = 1;

c = 101;

e = 201;

d = 102;

f = 202;

结果是x 和y都是 0 。volatile 关键字也可以禁止代码重排序。

2)volatile关键字之前的代码可以重排序 

下面验证volatile之前的代码还是可以出现代码重排的效果,重排序后的顺序为:

t1t2

x = b;

y = a;

a = 1;

b = 1;

c = 101;

e = 201;

volatile  d = 102;

volatile  f = 202;

测试(注意注释,标明了和之前代码的区别): 

public class Test2 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long c = 0;
    volatile private static long d = 0;//d 加上 volatile 关键字
    private static long e = 0;
    volatile private static long f = 0;//f 加上 volatile 关键字
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count ++;
            Thread t1 = new Thread(() -> {
                a = 1;
                c = 101;
                x = b;//x放在 volatile 关键字d之气那
                d = 102;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                e = 201;
                y = a;//y放在 volatile 关键字f之气那
                f = 202;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count= " + count+" " + x + "," + y +"";
            if (x == 0 && y == 0){
                System.err.println(showString);
                break;
            }else {
                System.out.println(showString);
            }
        }

    }
}

 运行结果:

由此可知,volatile关键字之前的代码重排序了。

3)volatile 关键字之后的代码可以重排序 

下面验证 volatile 之后的代码 还是可以出现代码重排序的效果,重排后的顺序如下:

t1t2
volatile    c=101;volatile     e=201;
x=b;y=a;
a=1;b=1;
d=102;f=202;

测试代码:

public class Test3 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    private static long d = 0;
    volatile private static long e = 0;
    private static long f = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count ++;
            Thread t1 = new Thread(() -> {
                c = 101;//volatile
                a = 1;
                d = 102;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                e = 201;//volatile
                b = 1;
                f = 202;
                y = a;
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count+" " + x + "," + y +"";
            if (x == 0 && y == 0){
                System.err.println(showString);
                break;
            }else {
                System.out.println(showString);
            }
        }

    }
}

运行结果如图:

由此可知,volatile关键字之后的代码重排序了。 

4)volatile关键字之前的代码不可以重排到volatile之后 

下面验证 volatile 之前的代码不可以重排到 volatile 之后,x 和 y 同时不为 0 的情况不会发生。 

public class Test4 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    volatile private static long d = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            count ++;
            Thread t1 = new Thread(() -> {
                x = b;
                c = 101;
                a = 1;
            });
            Thread t2 = new Thread(() -> {
                y = a;
                d = 201;
                b = 1;
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count+" " + x + "," + y +"";
            if (x != 0 && y != 0){
                System.err.println(showString);
                break;
            }else {
                System.out.println(showString);
            }
        }

    }
}

程序运行后,x和y同时不为0的情况没有发生,也就是同时等于1的情况不会出现,所以volatile关键字之前的代码不可以重排到volatile之后。 

5)volatile 关键字之后的代码不可以重排到volatile之前 

x和y的值永远不可能同时为0; 验证:

public class Test4 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    volatile private static long d = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            count ++;
            Thread t1 = new Thread(() -> {
                a = 1;
                c = 101;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                d = 201;
                y = a;
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count+" " + x + "," + y +"";
            if (x == 0 && y == 0){
                System.err.println(showString);
                break;
            }else {
                System.out.println(showString);
            }
        }

    }
}

程序运行后,x和y的值永远不可能同时为0,所以关键字volatile之后的代码不可以重排到volatile之前。

6)synchronized关键字之前的代码不可以重排到synchronized之后

synchronized关键字也具有禁止代码重排的功能。

下面验证synchronized之前的代码不可以重排到synchronized之后,所以x和y同时不为0的情况不可能发生,也就是同时等于1的情况不会出现:

public class Test4 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    x = b;
                    synchronized (this) {
                    }
                    a = 1;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    y = a;
                    synchronized (this) {
                    }
                    b = 1;
                }
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count + " " + x + "," + y + "";
            if (x != 0 && y != 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }

    }
}

程序运行后,x和y同时不为0的情况没有发生,也就是x和y不可能同时为1,所以 synchronized关键字之前的代码不可以重排到synchronized之后。

7)synchronized关键字之后的代码不可以重排到synchronized之前 

下面验证 synchronized关键字之后的代码不可以重排到synchronized之前 ,x和y的值永远不可能同时为0:

public class Test4 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    synchronized (this) {
                    }
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    synchronized (this) {
                    }
                    y = a;
                }
            });
            t1.start(); // t1和 t2 以异步的方式执行
            t2.start(); // t1和 t2 以异步的方式执行,如果代码重排,则x和y的值都有可能是0
            t1.join(); // 如果 t1存活则等待,销毁则继续向下执行
            t2.join(); // 如果 t2存活则等待,销毁则继续向下执行
            String showString = "count= " + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }

    }
}

程序运行后,x和y的值永远不可能同时为0,所以synchronized关键字之后的代码不可以重排到synchronized之前。

8)总结 

synchronized关键字的作用是保证同一时刻只有一个线程可以执行某一个方法或者某一个代码块。synchronized可以修饰方法以及代码块,随着JDK版本的升级,synchronized在执行效率上得到了很大提升。它包含了三个特征:可见性,原子性和禁止代码重排序。

volatile关键字的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。它也包含三个特征:可见性,原子性和禁止代码重排序。

总结一下,volatile和synchronized关键字的使用场景如下:

1)当想实现一个变量的值被更改,而其他线程能获取到最新的值时,就要对变量使用volatile;

2)如果多个线程对同一个对象中的同一个实例变量进行写操作,为了避免出现非线程安全问题,就要使用synchronized。