上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile。volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程。从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。
旧的内存模型:保证读写volatile都直接发生在main memory中。
在新的内存模型下(1.5)对volatile的语义进行了修补和增强:如果当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的所有变量值现在都可以保证对 B 是可见的。
一句话:volatile保证可见性,但不能保证原子性。
原子性的:一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。volatile变量的非原子性最容易被忽略。
可见性:指一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。
volatile的非原子性
变量被定义为volatile并不能保证对其所有操作是原子的,由于非原子性,因此volatile并不能保证多线程并发的安全性。如下面的代码:
public class Test implements Runnable{
public volatile int race = 0;
@Override
public void run() {
increase();
}
private void increase() {
race ++;
}
public static void main(String[] args) {
Test t = new Test();
Thread [] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(t);
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 保证打印的时候1000个线程都已经执行完毕
System.out.println(t.race);
}
}
这段代码开启了1000个线程,对race变量进行自增操作。理论上,线程安全的话,执行结果应该是1000。但实际上执行得到的结果都是一个小于1000的值。
分析一下上面案的代码,问题就出在了race++这句代码。它不是原子操作。这句代码实际上是分为三个操作的:读取race的值、进行加1操作、写入新的值。
显然可以看出来,将变量定义成vilatile也不能保证原子性:
线程1先读取了变量race的原始值,然后线程1被阻塞了;线程2也去读取变量race的原始值,然后进行加1操作,并把+1后的值写入工作内存,最后写入主存,然后线程1接着进行加1操作,由于已经读取了race的值,此时在线程1的工作内存中race的值仍然是之前的值,所以线程1对race进行加1操作后的值和刚才一样,然后将这个值写入工作内存,最后写入主存。这样就出现了两个线程自增完后其实只加了一次。究其原因是因为volatile不能保证原子性。
可以将自增操作改为同步代码块即可解决。
private synchronized void increase() {
race ++;
}
volatile的可见性
一个线程修改了某个volatile变量的值,这新值对其他线程来说是立即可见的。
boolean ready;
// thread 1
while (!ready) {
doSomthing();
}
// thread2
ready = true;
这是销毁线程的通用方法。但是存在问题是ready变量改为true还没来得及写入主存,就转到其他线程执行了,这时还会进入循环。这时,volatile的作用就体现出来了。volatile变量保证了他在一个线程里面修改后会立即被其他线程得知。
volatile变量的使用场景这篇文章写得很详细:Java 理论与实践: 正确使用 Volatile 变量