volatile关键字理解
今天抽空看了一些关于volatile的解析,让我对它的使用有了一定了解,volatile的使用场景一般用于并发条件下,属于轻量级并发锁(也可以理解为乞丐版的java并发锁)。
volatile自身有三大属性,分别是:可见性,不保证原子性和禁止指令重排。这些属性既给我们带来便利,但也有不足,所以只用把它用在最合适的地方,就会发挥它最好的效果。
一、可见性。
什么叫做可见性呢,顾名思义就是一个线程做的事务,其它的线程可以看到最后的结果。简单的描述下线程的执行图示:
当本地线程从主内存进行副本拷贝时,如果不添加volatile关键字,可能会造成某个线程拿到没有更新过的旧数据。加上volatile后,当一个线程修改主内存的数据后会通知其他线程立刻刷新主物理内存的最新值。从而保证了在多线程的环境下,数据的一致性。
二、不保证原子性
原子性是不可分割,完整性,某线程在做某一个业务时,中间不可内加塞或分割,需要一个整体完整,要么同时成功,要么同时失败。
为什么volatile不能保证原子性呢,在一般的 i= 0;是一个正常的原子操作,但是像i++;这种就是分原子性操作,它可以拆分为三个步骤,先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。 当在多并发的条件下,a线程对开始i值进行修改,当发生阻塞的情况下,还未把值写入缓存中,这是b线程开始对i进行修改。当把i修改完成之后,这是通知发挥可见性通知其他的线程,而a线程在阻塞后,这时拿到的值已经是被b线程修改之后的值了,所以在进行操作之后显然得到的不是正确的值。 那个如何保证数据的原子性呢,一般想到的是,在方法前加上synchronized关键字,但是问题来了,当加上synchronized后,会使整个方法变得很深重,那有没有性能更高的方法来替代呢?
当然是有了,比如加上(juc)下的AtomicInteger 关键字也可以保证数据的原子性,AtomicInteger 具体熟悉,小伙伴们可以去网上做一些深入的了解。
三、禁止指令重排
稍微对jvm和jmm有一定了解的小伙伴们, 知道当编译器在执行我们的源码时,会对源码进行一定的优化,和执行重排序。
大致是分成:源代码——>编译器代码优化重排——>指令并行的重排——>内存系统的重排——>最终指令
但是在数据重排的过程中,需要优先考虑数据的依赖性,比如一个方法
i = 1; ----(a)
j= i + 2; ----(b)
k =3; ----(c)
这个方法的执行顺序可以是 acb ,cab,abc,但是不能为bac或者bca,因为在执行b过程的时候,i还没有被初始化赋值。所以这就是提现了数据的依赖性。
volatile的内存屏障
对volatile变量进行写操作时加store屏障指令将工作内存的变量刷新到主内存中,在进行读操作时会加上load屏障指令,从主内存读取最新的共享数据。
另外提一个在多线程情况下可能会遇到的问题
在使用单例模式的时候,在单线程情况不会出现问题,但是在多线程下,单例可能就会变成多例模式,会实例化多个对象,通常的解决方法也可以加上synchronized关键字,但是同样的问题就是synchronized锁太重了,会使方法性能下降.另一个解决方法是使用DCL(双端检锁)的方法,同时要加上volatile关键字来保证数据的一致性。
正常的实例化过程是
instance = new SingletonDemo(); 可分三步
memory = allocate() 1 分配内存空间
instance (memory) 2初始化对象
instance = memory 3 指向内存地址,此时instance!=null
在多线程的情况下会使这三个步骤进行重排序,所以在使用一般的单例模式时,会创造出多个对象出来,从而不能保证单例性。