当前位置:K88软件开发文章中心编程语言JavaJava01 → 文章内容

JAVA并发编程4_线程同步之volatile关键字

减小字体 增大字体 作者:佚名  来源:网上搜集  发布时间:2019-1-4 7:54:11

-->

上一篇”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并不能保证多线程并发的安全性。如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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不能保证原子性。

可以将自增操作改为同步代码块即可解决。

1
2
3
private synchronized void increase() {  
        race ++;  
    }

volatile的可见性

一个线程修改了某个volatile变量的值,这新值对其他线程来说是立即可见的。

1
2
3
4
5
6
7
boolean ready;  
// thread 1  
while (!ready) {  
       doSomthing();  
}  
// thread2  
ready = true;

这是销毁线程的通用方法。但是存在问题是ready变量改为true还没来得及写入主存,就转到其他线程执行了,这时还会进入循环。这时,volatile的作用就体现出来了。volatile变量保证了他在一个线程里面修改后会立即被其他线程得知。

volatile变量的使用场景这篇文章写得很详细:Java 理论与实践: 正确使用 Volatile 变量


JAVA并发编程4_线程同步之volatile关键字