在Java多线程编程中,volatile关键字可以保证可见性和有序性,不能保证原子性。
可见性
可见性指的是当有一个线程修改某个变量,其它的线程可以及时知晓这个变量被修改了。
在单线程中,如果某个对象的变量通过set方法修改了,比如int a=1,a=a+1,此时a为2,后面的程序在访问变量a的时候那就是2;但是对于多线程的情况下,并非如此。
在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,刷到主内存
}
}
输出按照我们之前所说的,此时线程获取到的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,刷到主内存
}
}
输出:volatile解决可见性
可以使用volatile关键字解决内存的可见性问题。当使用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,刷到主内存
}
}
输出:
主线程修改为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);
}
}
输出不是我们预取的结果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());
}
}
结果如我们预期,所以volatile 无法保证线程安全,如若需要需要修改为原子类。