Java并发编程之volatile

477 阅读3分钟

前言

volatile是Java虚拟机提供的一种轻量级的同步机制。我们都知道volatile只保证了可见性和有序性。而我们知道线程安全是需要同时满足可见性、有序性、原子性的三个特性的。所以我们说volatile是保证不了线程安全的。那么本文就想来谈一谈为什么volatile保证不了线程安全。

并发编程3个基本概念

1. 原子性

即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

3.有序性

即程序执行的顺序按照代码的先后顺序执行。

volatile保证可见性

可见性

未使用volatile示例:

package com.example.javatutorial.thread;

public class VolatileDemo {

    public static class Demo{
        public int num = 0;
        public void change() {
            num = 1;
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+ "\t start");
                Thread.sleep(3000);
                demo.change();
                System.out.println(Thread.currentThread().getName()+ "\t update value:" + demo.num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread1").start();

        while (demo.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + "\t num value is" + demo.num);
    }
}

执行结果:

image.png 可以看见我们在thread1线程内修改了num变量为1,但在主线程中程序被阻塞在了while循环中,也就是说主线程无法感知到num值发生更改。 使用volatile示例:

package com.example.javatutorial.thread;

public class VolatileDemo {

    public static class Demo{
        public volatile int num = 0;
        public void change() {
            num = 1;
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+ "\t start");
                Thread.sleep(3000);
                demo.change();
                System.out.println(Thread.currentThread().getName()+ "\t update value:" + demo.num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread1").start();

        while (demo.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + "\t num value is" + demo.num);
    }
}

执行结果: image.png 我们可以看到主线程立即输出了内容,说明主线程已经可以感知到num值的变化。

volatile不保证原子性

示例代码:

package com.example.javatutorial.thread;

public class VolatileDemo2 {

    public static class Demo{
        public volatile int num = 0;

        public void change() {
            num++;
        }
    }

    public static void main(String[] args) {

        Demo demo = new Demo();

        for (int i = 1;i <= 10;i++) {
            new Thread(() -> {
                for (int j=0;j<1000;j++) {
                    demo.change();
                }
            },"thread" + i).start();
        }
        while (Thread.activeCount()>2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " finally num value is " + demo.num);
    }
}

执行结果:

image.png

可以看到num值并不是10000,出现了随机性。这是因为num++并不是一个原子操作,这个操作由读取、加、赋值三步组成,所以结果并不能达到预期值10000。

volatile保证有序性

volatile保证有序性是通过禁止指令重排序实现的。重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。
  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。 从规则里我们可以看出,在单线程下结果的正确性一定可以得到保证,但是在多线程环境下,结果就不一定了。 示例代码:
public class VolatileDemo4 {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            Thread one = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                b = 1;
                y = a;
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            }
        }
    }
}

执行结果: 2300b53bb7d114eb57615d7fb25219f.png 可以看到在执行了n多次之后出现了x=0,y=0的结果。也就是说这两个线程里的代码发生了重排序。

小结: 由于volatile关键字并不能保证原子性,所以就无法满足并发编程的3个特性,也就是说volatile关键字不能保证线程安全。