关于java多线程中volatile关键字的疑惑

cook702 发布于 2016/02/24 13:56
阅读 398
收藏 0
最近找工作,面试体重提到这个关键字的用法,因为平时很少用到,所以对它的用法不是很了解,于是从网上查找相关资料,对它的使用有了一定了解,但是有一点仍然很困惑,随着知识的深入,感觉是对自己以前工作中的多线程同步写法的一种否定。java内存模型中提到每个线程有自己的工作内存,线程不能直接操作共享内存,而是先修改工作内存,然后再同步到共享内存。资料说volatile保证了共享数据的可见性。如果不对变量使用此关键字并且不加锁变量,多个线程同时对变量进行操作时就可能出现数据不同步的问题。

线程的工作内存是对共享内存的一个拷贝,当线程运行的时候,首先读取工作内存的数据,如果没有才会从共享内存进行拷贝,我们以前有一种写法:

public class VolatileTest {


   public static void main(String[] args){
        Thread t1 = new Thread(A.getInstance());
        t1.start();
    Thread t2 = new Thread(new B());
        t2.start();
    }
}

class A implements Runnable {
    private Boolean flag = true;
    private static A instance = new A();

    private A(){
    }

    public static A getInstance(){
        return instance;
    }

    public void run() {
        while(flag){
            System.out.println("thread a is running!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public void stop(){
        flag = false;
    }
}

class B implements Runnable { @Override
    public void run() {
        int count = 0;
        while(count < 5){
            System.out.println("thread b is running!");
            count++;
            try{
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        A.getInstance().stop();
        System.out.println("thread b stop!");
    }  
}

如果按照上述说法,a.flag没有加volatile关键字,那当线程t1执行时,从共享内存拷贝变量flag为true,当线程t2把flag变量变为false后,此时应该刷新了共享内存,但是线程t1此时仍然读取工作内存,flag仍然为true,导致线程t1仍然循环,而实际的结果却是想法的,t1线程正常结束了。网上有人说出现t1一直循环的情况是可能存在的,我这里的主要困惑是,对于没有加volatile关键字的变量,线程是如何在共享内存和工作内存之间同步的,比如t1开始时从自己的工作空间读取flag的值,当别的线程对此值改变并且更新了共享内存,那线程t1是如何知道更新的,还是说一直不知道。如果一直不知道的话,就会出现t1会一直循环执行下去,可实际结果却不是这样的。

由上述问题,衍生出如下问题:

如果线程t1有一个map,没有加volatile,一个线程提供了它的读写操作方法,如下:

public synchronized void writeMap(){
    map.put(obj);
    ...
}

public map getMap(){ //没有加synchronized关键字
    return map;
}


如果此时有一个线程t2一直在调用t1的getMap()方法,而如果有其它线程此时调用了t1的writeMap()方法,那是不是t2获得的map也不是最新的,因为它第一次读取完map后会加载到自己的工作内存,此后会一直从工作内存中读取,那最新的数据就不能读取出来,按照这种理解,必须对map加volatile关键字,保证读取时必须从贡献内存读取最新的数据。而实际操作中,结果却是正确的。

在此之前,我们的多线程代码编写,基本上没有接触过volatile,通过这次接触,感觉之前的代码都有可能会出现我上述说的这种问题,可能是我哪里理解有误,还希望有人指点一下,不胜感激!

加载中
0
ksfzhaohui
ksfzhaohui

volatile可以保证变量的可见,不能保证同步;添加了volatile可以保证读取到最新的值,不加的话并不是说不能读取到最新的值,只是不是及时的。至于是不是可以一直循环,可以参考java并发编程实战:


后面说map,不知道你的实现类是不是hashMap,如果是的话,hashMap是非线程安全的,肯定是需要锁的,不管你是get还是put


c
cook702
"不加的话并不是说不能读取到最新的值,只是不是及时的",我就是对这个工作内存和共享内存同步的同步过程不是很了解,具体在什么时候更新,网上没有找到答案。
0
樂天
樂天

可以看一下《java并发编程实战》 

http://book.douban.com/subject/10484692/

c
cook702
谢谢
0
调皮的XD
调皮的XD

volatile关键的作用是

1:保证变量可见性

2:禁止指令重排序

0
xpbob
xpbob
你还没明白为什么用这个关键字,前提条件是多个cpu,每个cpu都有自己的寄存器,你的一些用到的变量是先回在寄存器中的,举个例子a,b两个cpu,共同操作num=8这个变量,a取到进行自增,a的cpu的寄存器的值是9,同步到内存,b取到num是9,然后继续自增然后同步到内存,此时内存的值是10,当a再次进行自增的时候,看到寄存器里有这个值(num为9),会把这个值变成10,这样和内存中的值就出现了偏差,当你用volatile的时候,cpu就不会取寄存器里的值,会一直到内存中取,这样就保证了可见性,但是可见性和原子性不是一回事,用volatile只能保证你在同步执行下正确,但是你多线程不同步的话,volatile也没用。就是a把num自增的时候,b同时取到num自增,然后放回内存的时候,就看谁后放就是谁的值。
xpbob
xpbob
回复 @cook702 : T1如果能感知到变化,那么必须保证变量的可见性,加锁或者volatile,每次都会去内存中取,而不是用寄存器
c
cook702
我明白您说的意思,就如我所说的例子,A.flag初始为true,我的理解是T1线程会吧flag=true放入寄存器,因为flag没有加volatile关键字,当T2线程把flag改为false后,T1线程是感知不到flag的变化的,它会一直以为flag还是自己寄存中的true值,这样会导致T1线程一直循环。如果T1能感知到flag的变化,它又是怎样的一个过程能?
0
尚浩宇
尚浩宇
这么说吧。你的类A是单例的,所以无论多线程还是单线程,你的A.getInstance()获取到的都是同一个引用地址,自然无论在哪个线程类A里的引用属性对象flag也都是指向同一个引用,所以只要flag 发生变化,其他线程都是可见的。你第二个问题也是同样的,你synchronized 的是保证了每次添加数据时是串行执行,但是map是一个,没有发生改变,引用地址自然也不会改变,所以其他线程取到的都是最新的。如果你想实现你预测的那样,第一把类A从单例变成多例,第二用Collects类包装一个你的map
尚浩宇
尚浩宇
回复 @cook702 : 要想真正弄明白,靠大家一人一句是会扭曲你的想法的,毕竟个人有个人的见解,建议你看下虚拟机规范,了解下什么是工作内存,什么共享内存,也就是寄存器的处理
c
cook702
以前我也是这样认为,但是现在了解了线程的工作内存和共享内存,感觉以前的认识都被否定了似的,请关注我对上述问题的追问。
返回顶部
顶部