Java并发编程(三)volatile关键字

1. 为什么需要volatile关键字

1.1 所谓多线程变量“不可见”问题

volatile关键字修饰的变量可以在多个线程之间保持“可见性”。这样的解释有些抽象,我们来看一个例子。
假设下面这种场景:我们来模拟一个网站访问计数器。定义一个NetAccessor类,类中有变量counter用来记录网站访问总数,默认counter等于0。

public class NetAccessor {

    public static int counter = 0;

    public static void access(){
       counter = counter +1 ;
    }
}

假设有两个客户访问了这个网站,即有两个线程A和B,A会调用access方法给counter加1,然后B也会调用access给counter加1,此时按我们的设想,网站的总访问量应该为2。代码如下所示,运行一下看看结果是多少。

public class NetAccessor {

    public static int counter = 0;
    
    public static void access(){
        counter = counter + 1;
    }
    
    public static void main(String[] args) {
        Thread A = new Thread(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                access();
            }
        });
        Thread B = new Thread(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                access();
            }
        });
        A.start();
        B.start();
        System.out.println("Now the total access count is : "+counter);
    }
}

多次运行代码,有时我们会看到正确的结果

Now the total access count is : 2

有时,我们会看到下面这种结果

Now the total access count is : 1

也就是说,多个线程间共享的变量在被A线程修改后,可能B线程并不知道该变量已经被修改了。也就是存在变量在线程间“不可见”的问题。这是为什么呢?
原因是:在JVM中,为了提高各个线程的执行效率,每个线程都会从主线程的内存中复制自己所需要的变量到自己的CPU Cache中,这样对这些变量进行操作时,不用来回切换到主线程内存,可以提高执行效率(见下图)。比如上例,在线程A启动时,它会从主线程中复制变量counter到自己的内存空间,此时counter是0,然后线程B启动时,也会从主线程复制变量counter,此时counter还是0,而当线程A操作counter+1将counter变为1时,B线程对线程A中的counter是不可见的,还是0,所以线程B再执行counter+1,counter的值还是1。此时两个线程无论谁将counter的值flush到主线程中,counter都只会是1而不会是2(什么情况会是2呢?恰巧线程B在读取counter之前,线程A已经完成了操作counter+1,并flush到了主线程上,这时B拿 到的counter变量值就是1了。有点儿绕,简而言之,就是多个线程间的共享变量存在“不可见”问题。)


volitale原理

1.2 解决“不可见”问题的方法

  • 使用synchronized关键字,对上例中操作共享变量counter进行加锁,保证同时只有一个线程操作该变量。但这种方式性能较低,如果有成千上万个线程操作同一变量,则只能有一个得到锁,其它的都得阻塞等待。
  • 使用volatile关键字,让共享变量counter在多个线程间可见。子线程在读取counter的值前,会直接从主线程内存中读取,在写入counter的值时,会直接写到主线程内存中。这样牺牲了一定的并发性能,但保证了该共享变量在多线程间可见(其实就是用的都是主线程内存中的变量,而不是复制到各线程CPU Cache中的变量了)。
    改造上例中代码,只需加个一个volatile进行修饰共享变量即可
public class NetAccessor {

    public static volatile int counter = 0;

    public static void access(){
       counter = counter +1 ;
    }
}

2.volatile关键字在jdk5.0之后新增的特性

在JDK5.0之后,volatile关键字的意义不只是在于“从主线程内存中直接读写变量”那么简单了。还涉及以下两个新的规则:

  • 如果线程A对某个volatile变量执行写操作,然后线程B对同一变量执行读操作,那么所有A线程中在执行写操作前的变量,也会随着该volatile变量flush到主线程中,对线程B保持可见性。
  • 读写的顺序是非常重要的。线程A对某个volatile变量执行写操作之后的变量,对B线程还是不可见的。
    运用这种特性,开发人员可以轻松实现以下交换逻辑,只要注意变量修改的顺序,无需再对每个要可见的变量都加上volatile关键字:
public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}

3.volatile关键字不能保证原子性

volatile虽然可以让多个线程之间实现共享变量的可见性,但却不能保证原子性。
比如:我们有100个线程,共享同个volatile变量counter(初始为0),这100个线程执行同样的操作,即在counter基础上累加1000,按照设想,如果volatile变量具有原子性(同时只有一个线程访问),则最终的值应该有100000。那么结果如何呢?请看示例代码:

public class VolatileNoAtomic extends Thread{
    private static volatile int count;
    private static void addCount(){
        for (int i = 0; i < 1000; i++) {
            count++ ;
        }
        System.out.println(count);
    }
    
    public void run(){
        addCount();
    }
    
    public static void main(String[] args) {
        
        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }
        
        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }
            
}

结果可以看到:大多数情况下并不是我们期望的输出结果100000。这是因为volatile修饰的变量并不具备原子性,多个线程可能同时读取到同个volatile变量的值 。

4.解决原子性问题

JDK提供了Atomic系列类用于对基本数据类型(如int, long等)实现原子操作,使用同一时间只能有一个线程操作该变量。对上面示例进行修改如下:

public class VolatileNoAtomic extends Thread{
    private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount(){
        for (int i = 0; i < 1000; i++) {
            count.incrementAndGet();
        }
        System.out.println(count);
    }
    
    public void run(){
        addCount();
    }
    
    public static void main(String[] args) {
        
        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }
}

这样便可以解决原子性问题,但是要注意的是,Atomic对象的原子性只针对单一次原子操作,并不能保证多个操作的原子性,此时应该考虑使用锁了。

注意

以上内容源自互联网相关资料及本人学习与工作经验,仅为学习及技术分享所用,切勿用于商业用途,转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容