volatile详解(一)

因为看了多个博主写的单例模式的博文仿照了一个双重校验锁的单例方法导致系统出现问题特此针对volatile进行整理总结。

一、基本概念

Java 内存模型的三种特性 可见性、原子性和有序性。这三种特性主要体现在多线程环境下对变量的操作,这些变量包括:实例字段、静态字段、构成数组对象的元素。这些变量都保存在堆中,堆是线程共享的。那么这些变量在多线程环境下就有可能出现所谓“线程不安全”的问题。 局部变量和方法参数是线程私有的保存在栈中,不会出现线程安全问题。

 

可见性:

在多线程中如果多个线程操作一个数据时,由于修改后并没有通知到其他线程,会导致数据最后的结果与我们想要的结果大相径庭。为了保证多个线程之间对数据写入操作的可见性,必须使用同步机制。

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的数据,另一个线程马上能看到。

例:

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

  此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

可见性补充说明:

1.多个线程同时修改全局变量,其中一个线程修改后还没有刷新到主内存就被另外一个线程拿走使用,会产生脏数据。

2.其中一个线程修改后已经刷新到主内存,但是另外一个线程有延迟,没有拿到主内存的值,还是用之前本地内存里边的值,也会产生脏数据。

3.volatile关键字就是保证全局变量可以实时同步的刷新到主内存,并且直接分派到本地内存副本中,实现可见及同步。

 

原子性:

原子操作(atomic)就是不可分割的操作这里说的原子性和数据库事务的原子性基本一样,原子性指的是一次数据的操作要么都成功要么都不成功。这个特性的重点在于不可中断。

例:

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行 m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

但是,声明并赋值就不是一个原子操作:

int  n=6;//这不是一个原子操作

对于这个语句,至少有两个操作:①声明一个变量n ②给n赋值为6——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用,就可能会导致不稳定的结果出现。

m++; //非原子操作

第一个例子属于原子操作(m不是long和double),a++相当于a=a+1属于非原子操作,而非原子操作在多线程中会出现问题。线程A执行上述代码,从内存中读取这三个变量的值,在读取的过程中,此时线程B也读取了某一个变量的值,此时虽然线程B的这个操作并不会对线程A的结果产生影响,但是线程A的原子性已经不存在了,在底层CPU执行的时候,就会涉及到切换线程A、B。并且,对A要进行中断,所以线程A的原子性就被破坏了。理解这一点,也就会理解关键字volatile并不能保证原子性,保证原子性需要加锁!!!

在单例模式中,如果不是使用加锁的方法,就会因为没有保证原子性,而使得对象会被创建多个。

public class Singleton1 { private volatile static Singleton1 singleton=null; private static boolean flag = true; private Singleton1 (){ } public static Singleton1 getSingleton() { if (singleton == null) { singleton = new Singleton1(); } return singleton; }

虽然使用了关键字volatile,但是两个线程会同时执行getSingleton()方法,并且可以都通过为null的判断,然后进行对象的创建,因为getSingleton()这段代码的原子性没有被保证,保证的方式就是为这个方法加锁。

 

还有一个例子就是32bit的jvm操作long类型数据。Long需要64位,多线程环境下,对long类型的数据进行加运算,有可能一个线程读取的数据是另一个线程修改“一半”的数据,比如只修改了低位,还没有修改高位,此时数据被另一个线程读取,那么结果自然就是错的。解决办法就是在修改long数据的方法上加锁,保证此方法被某一线程调用时,不被其他线程干扰。

有序性:

说道有序性就必须了解下内存模型中的指令重排

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。比如,这一段代码:

int a ;   // 语句1 

a = 8 ;   // 语句2

int b = 9 ;     // 语句3

int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。但是,由于指令重排

的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。

由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

在Java中使用volatile和synchronized来保证多线程之间的有序性。Volatile通过加入内存屏障指令来禁止内存的重排序。Synchronized通过加锁,让一段代码只能由一个线程来执行。这两个区别就是,synchronized可以修饰一段代码,或者一个方法。但是volatile只能修饰一个变量。

后续补充关于volatile内存屏障问题,碎觉~

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值