volatile 关键字:
当多个线程进行操作共享数据时,可以保证内存中的数据可见。 相较于 synchronized 是一种较为轻量级的同步策略。
缺点:
1. volatile 不具备“互斥性”
2. volatile 不能保证变量的“原子性”
内存可见性是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的。
我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的 volatile 变量。
下面用一个案例说明,代码如下:
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
if (td.isFlag()) {
System.out.println("------------------进来");
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
// 延迟一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
flag是共享数据存在于主存中,ThreadDemo启动以后主要是来改变flag的值,而main函数本身也是一个线程来读取flag的值。为了让效果更加明显,在ThreadDemo线程里面run方法里面休眠了1秒,目的是先让flag的值先改变,不然main线程里面可能会先读取到flag的值。按理说ThreadDemo线程先改变了flag的值为true,然后while(true)里面读取flag的值应该是true,然后输出,但是运行结果并不是这样。
运行结果如下。
为什么会这样呢?
首先ThreadDemo线程先从主存中读取共享数据的内容,然后放入自己的工作内存中,然后再改变flag的值,但是这时的值还没有写回主存中,main就读取的flag的值,此时为false,当ThreadDemo把值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,所以while(true)读取到的值一直是false。
解决方法1:synchronized锁
while (true) {
synchronized (td) {
if (td.isFlag()) {
System.out.println("------------------进来");
}
}
}
synchronized 实际上是对访问修改共享变量的代码块进行加互斥锁,多个线程对synchronized代码块的访问时,某一时刻仅仅有一个线程在访问和修改代码块中的内容(加锁),其他所有的线程等待该线程离开代码块时(释放锁)才有机会进入synchronized代码块。
某一个线程进入synchronized代码块前后,执行过程入如下:
a.线程获得互斥锁
b.清空工作内存
c.从主内存拷贝共享变量最新的值到工作内存成为副本
d.执行代码
e.将修改后的副本的值刷新回主内存中
f.线程释放锁
解决方法2:volatile修饰变量
private volatile boolean flag = false;
volatile如何实现可见性:
volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。
volatile不能保证变量更改的原子性:
比如sum++,这个操作实际上是三个操作的集合(读取sum,sum加1,将新的值写回sum)
volatile只能保证每一步的操作对所有线程是可见的,但是假如两个线程都需要执行sum++,那么这一共6个操作集合,之间是可能会交叉执行的,那么最后导致sum的结果可能会不是所期望的。
解决方法3:java concurrent类来实现共享变量的可见性