在Java多线程编程中,volatile关键字可以保证可见性有序性,不能保证原子性

可见性

可见性指的是当有一个线程修改某个变量,其它的线程可以及时知晓这个变量被修改了。

在单线程中,如果某个对象的变量通过set方法修改了,比如int a=1,a=a+1,此时a为2,后面的程序在访问变量a的时候那就是2;但是对于多线程的情况下,并非如此。

Java中多线程可见性

在cpu执行指令的时候,是需要读取内存中的数据的,然而cpu的速度明显高于内存,如果让cpu直接与内存交互是会降低cpu的执行效率,所以,在cpu和内存中间有一个高速缓存区cache。

当执行线程的时候,需要将主内存中的数据复制一份到高速缓冲区cache中执行,执行完成后cpu再将高速缓冲区的内容刷到主内存中。

假设有两个线程thread1,thread2,共享变量count=0,并且都要执行count=count+1,其中thread1在cpu1,线程2在cpu2中。

thread1执行count=count+1,此时count=1并将count刷入到主内存中

threa2中的count值在高速缓冲区cache中还是0,也执行count=count+1,也将count刷入到主内存中

两个线程执行完毕此时count是1而不是2。

来看一个例子

public class VolatileExample extends Thread {
    private int count = 1;

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开启线程");
        try {
            Thread.sleep(1000*2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程当前的count值"+count);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample task1 = new VolatileExample();
        task1.start();
        Thread.sleep(1000);
        System.out.println("主线程修改为0");
        task1.setCount(0);//修改为0,刷到主内存

    }
}
输出

Java中volatile关键字

按照我们之前所说的,此时线程获取到的count值不应该是1吗?但是结果线程为什么是0?volatile关键字失效了吗?

这是因为Thread.sleep会刷新内存,当线程sleep的时候高速缓冲区cache又重新与主内存的内容同步了一遍,而当程序里面有while的情况下就没有机会刷新主内存,看下面的例子

public class VolatileExample extends Thread {
    private int count = 1;

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开启线程");
        while(count>0){
            //这里线程卡住,刷新不了主内存,所以会一直执行
        }
        System.out.println("线程当前的count值"+count);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample task1 = new VolatileExample();
        task1.start();
        Thread.sleep(1000);
        System.out.println("主线程修改为0");
        task1.setCount(0);//修改为0,刷到主内存

    }
}
输出:

Java中volatile关键字

volatile解决可见性

可以使用volatile关键字解决内存的可见性问题。当使用volatile修饰某个变量,工作线程会直接从主内存中取值,而不是各个线程的副本,如图所示:

Java中volatile关键字

我们修改上面的例子

public class VolatileExample extends Thread {
    volatile private int count = 1;

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开启线程");
        while(count>0){
            //这里会退出,因为直接读取主内存的值
        }
        System.out.println("线程当前的count值"+count);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample task1 = new VolatileExample();
        task1.start();
        Thread.sleep(1000);
        System.out.println("主线程修改为0");
        task1.setCount(0);//修改为0,刷到主内存

    }
}

输出:

Thread-0:开启线程
主线程修改为0
线程当前的count值0

volatile不保证原子性

原子性的意思就是保证线程安全,多个线程访问同一个变量,变量是按照命令依次执行修改,是安全的不会出现争用,但volatile不保证原子性,举例说明。

public class VolatileExample extends Thread {
    volatile public static int count = 1;

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开启线程");
        for (int i=0;i<10000; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName() + "线程当前的count值"+count);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample task1 = new VolatileExample();
        VolatileExample task2 = new VolatileExample();

        task1.start();
        task2.start();

        Thread.sleep(1000*2);
        System.out.println("主线程count值:" + count);
    }
}
输出
Java中volatile关键字

不是我们预取的结果20000,说明volatile 无法保证原子性。我们将count改为原子类试下:

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileExample extends Thread {
    volatile public static AtomicInteger count = new AtomicInteger();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开启线程");
        for (int i=0;i<10000; i++) {
            count.incrementAndGet();
        }
        System.out.println(Thread.currentThread().getName() + "线程当前的count值"+count);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample task1 = new VolatileExample();
        VolatileExample task2 = new VolatileExample();

        task1.start();
        task2.start();

        Thread.sleep(1000*2);
        System.out.println("主线程count值:" + count.toString());
    }
}
Java中volatile关键字

结果如我们预期,所以volatile 无法保证线程安全,如若需要需要修改为原子类。