volatile与synchronized谁更安全?

学习JMM时发现,并发程序下的数据一致性很重要,一个线程对某个数据做了修改,怎么保证另一个线程看到的这个数据是修改后的或者说是正确的?JMM的建立就是围绕三点:原子性、有序性和可见性。

      原子性指的是一个操作是不可被打断的,即多个线程共同执行时,某个线程的操作不会被其他线程干扰。有序性问题比较难理解,在程序执行时有可能会出现指令重排,重排后的指令与原指令顺序就不一定相同了。你肯定想问为什么要进行指令重排?按顺序一条一条执行不好吗?(我也是这样疑惑- -、),后来学习知道,指令重排是为了性能问题,关乎指令的“流水线”技术。第三个,可见性,指的是当一个线程修改了某个共享变量的时候,能否让其他的线程立刻知道这个共享变量被修改了?举个例子,线程t1和t2共享一个变量value,线程t1对value变量做读取操作,value变量被copy了一份副本到高速缓冲存储器中,此时如果t2线程对value变量做了修改,t1线程没有发现,而是继续对高速缓冲存储器上的“旧”的value值做操作,最后保存,那就出现问题了。当然上述说的这些可能情况都是指在多道并行程序中。

      回到问题,如何保证线程执行的原子性,有序性和可见性?某个共享变量的修改能让其他线程立刻知道?Java里有一个特殊的关键字“volatile”,用它来声明变量,就是告诉虚拟机这个变量可能某线程序或者线程修改。为了保证修改后的变量能被其他线程发现,虚拟机就会特别小心地处理,保证这个变量的可见性特点。

      也就是说,volatile修饰变量对于保证对该变量操作的原子性有很大的帮助,虚拟机在处理volatile修饰的变量时会特别小心。但是但是,volatile并不能代替锁,它不能保证一些复合操作的原子性,注意是复合操作。

      下面看个例子:

我们创建十个线程对value++,最后value的值应该是10000,但是结果却经常出现小于1000的值:

如果volatile能保证复合操作的原子性,那么十个线程对value的自增操作应该能达到10000,可是事实证明volatile无法保证。假设n(n<10)个线程同时读取value的值为0,各自对value 做++,并先后写入这个结果,最后的结果就是value做了n次++,但实际value的值只加了1。

      volatile还有一个很重要的特点是它能保证数据的可见性,我们再来看个例子:

上面这段代码用一个线程不断判断boolean变量state是否为true,只有当state为true时才执行输出语句输出value。主程序中我们在线程t1也就是读vaule线程执行后,对value进行赋值并修改state让线程输出value的值,可是这样的代码结果却不如我们所愿,即使state修改为true了,但线程会一直执行第11行的判断代码,它不知道state被修改为true了,所以一直不会输出value,大家可以自己试一下执行这段代码。

      那么怎么办?volatile对于保证变量的原子性,可见性和有序性都有很大的帮助。上面这段代码线程一直不输出value的原因是主线程修改了state为true后,线程t1不知道这个state被修改了,所以还是一直while循环判断state,如果我们对state变量前面加上volatile修饰,就可以解决这个问题:

 

综上所述,volatile并不能真正保证线程的安全,因为volatile修饰的变量,只是确保它的值被修改后,其他的线程能看到它被修改了,但如果两个或多个线程同时对volatile修饰的变量做修改,还是会出现“覆盖”的情况(也就是线程1对value做修改保存后,线程2对value的修改保存覆盖了线程1对value的修改)。

要解决上面的问题,即多个线程对同一变量做修改时,如果不会出错。思路就是要保证多个线程之间的操作时同步的,线程1对value做++操作时,其他线程既不能对value做读操作,更不能做写操作。这里用到一个关键字“synchronized”,实现对代码的加锁操作,synchronized给对象加锁,给实例方法加锁或者给类函数也就是给类加锁。我们来看下面的例子:

第五行我们定义了一个AddValue的对象ad,第11行每次我们对value做++前都要获得ad对象的锁,如果此时ad的锁在别的线程手上,那么当前线程就必须等待ad的锁,拿到锁后才能执行value++操作,这样就能保证线程之间的同步了,看右下角最后10个线程每个线程都对value做1000次++操作,最后value结果是10000。

同样synchronized可以修饰实例静态方法,如果我们没给实例方法加锁,那么程序可能会出现同样的问题:

如果我们加上synchronized修饰实例方法,则不会出现问题:

这里我们要特别注意一个问题!!synchronized修饰实例方法increase(),即在进入increase方法前,要获得该类的对象的锁,这里是da,因为我们注意main函数里21行的new Thread(ad),这样写才能保证多个线程它们获得的锁都是ad的锁,如果写成这样:

如果把new Thread(ad)写成new Thread(new AtomicityTest()),那么每个线程关注的对象就不是同一个了,而是各自的实例对象,他们关注的不再是同一把锁ad,而是各自的实例中的ad锁,这样运行就会出错。

      上面的错误解决方法可以对实例方法稍加修改,因为synchronized还可以修饰类函数,我们知道类函数是属于类的,它不属于任何一个对象,所以在synchronized修饰的类函数它要获得的锁就是类的锁,而不是对象的锁,所以不同实例对象它们执行increase前都是要获得类的锁,而类的锁就只有着一个类,只有一把。

 

synchronized明显比volatile更安全,因为synchronized不光能保证原子性,也能保证可见性和有序性,可见性是因为,当一个线程获得锁对一个变量做操作时,其他线程由于拿不到锁,无法访问变量,当拿住锁的线程对变量做完修改后,释放锁,其他线程只有在拿到锁后才能访问变量,这就能保证这个变量的值是最新的。对于有序性同样,一个线程拿到锁后,其他线程必须等待锁,所以对于这一堆需要同一把锁的线程而言,它们是串行执行的忙着就能保证有序性的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值