volatile的特性及原理分析

先说特点:

1 保证了所修饰变量的内存可见性

2 防止指令重排

3 修改的变量不具有原子性

可见性

可见性: 某线程修改共享变量的指令对其他线程来说是可见的,它反映的是指令执行的实时透明度。简单来说:当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

每个线程都有独占的内存区域,入操作栈、本地变量表等。线程的本地内存保存了引用变量在堆内存的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。这里必然有个时间差,在这个时间差内,该线程对副本的操作,对于其他线程来说是不可见的。

这里解释一下,比如线程A对变量的修改,都是先在本地内存修改,修改后再同步到堆内存去,若此时,线程B在线程A还未同步到堆内存去,去执行读操作,则读到的值是旧值,而不是A同步到堆内存的值。

而valatile修饰变量时,意味着任何堆变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排的发生。

举例代码:

calss LazyInitDemo{
	private static TransactionService service = null;
	
	public static TransactionService getTransactionService() {
      
		if(service == null) {
			synchronized(this) {
				if(service ==null) {
                // 如果返回对象不为空,按还初始化完成情况呢
					service = new TransactionService();
				}
			}
		}
		return service;
	}
}

上面代码会出现问题,如果使用者在调用getTransactionService()时,有可能会得到初始化未完成的对象。

原因:首先肯定跟Java虚拟机的编译优化有关,对于Java编译器来说,初始TransactionService实例和对象地址写到service字段并非原子操作, 且这两个阶段的执行顺序是未定义的。假设某个线程执行new getTransactionService()时,构造方法还未被调用时,编译器仅仅为该对象分配了内存空间并设置默认值。此时另外一个线程调用getTransactionService()方法,由于service!=null,但此时service对象还没有赋予真正有效的值,从而无法取得正确的service单例对象。这就是著名的双重检查锁定问题,即对象引用在没有同步的情况下进行读操作,导致用户可能会获取未构造完成的对象。

对此问题,一种较为简单的解决方案就是用volatile关键字修饰目标遍历,这样service就限制了编译器堆它的相关读写操作,防止对它的读写操作进行指令重排,确定对象实例化之后才返回引用。

防止指令重排

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

然而,指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果。我们来看看下面的例子:

 

例如如下代码:

boolean contextReady = false;

在线程A中执行:

context = loadContext();

contextReady = true;

在线程B中执行:

while( ! contextReady ){ 

   sleep(200);

}

doAfterContextReady (context);

以上程序看似没有问题。线程B循环等待上下文context的加载,一旦context加载完成,contextReady == true的时候,才执行doAfterContextReady 方法。

但是,如果线程A执行的代码发生了指令重排,初始化和contextReady的赋值交换了顺序:

boolean contextReady = false;

在线程A中执行:

contextReady = true;

context = loadContext();


在线程B中执行:

while( ! contextReady ){ 

   sleep(200);

}

doAfterContextReady (context);

这个时候,很可能context对象还没有加载完成,变量contextReady 已经为true,线程B直接跳出了循环等待,开始执行doAfterContextReady 方法,结果自然会出现错误。

修改的变量不具有原子性

看代码:

/**
 * 描述: volatile特性验证
 *
 * @author pengjie_yao
 * @date 2019/7/19 10:12
 */
public class VolatileTest {
    private static volatile long count = 0L;
    private static final int NUMBER = 10000;

    public static void main(String[] args) {
        Thread subtractThread = new SubtractThread();
        subtractThread.start();
        for (int i = 0; i < NUMBER; i++) {
                count++;
        }
        //等待减法线程结束
        while (subtractThread.isAlive()) {
            System.out.println("count最后的值为:" + count);
        }


    }

    private static class SubtractThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < NUMBER; i++) {
                    count--;
            }
        }
    }
}

多次执行之后,发现结果不为0.如果在count++和count--两处加锁操作,结果才会相同。对于读写操作的字节码如下:

getstatic        //读取静态变量(count)
iconst_1        //定义常量1
iadd               //count增加1
putstatic        //把count结果同步到主内存

虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让count自增了很多次。这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全.

所以对于说法“volatile是轻量级的同步方式”是错误的说法,它只是轻量级的线程操作可见方式,并非同步方式,如果是多写场景,一定会产生线程安全问题。如果是一写多读的并发场景例如CopyOnWriteArrayList,它在修改数据时会把整个集合的数据全部复制,对写操作加锁,修改完成后,再用setArray()把array指向新的集合。使用volatile可以使线程尽快地感知array修改,不进行指令重排,操作后堆其他线程可见。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
    private transient volatile Object[] array;
    
    final void setArray(Object[] a) {
        array = a;
    }
.....
}

那么继续无法保证原子性,那么如何才能保证原子性呢,有兴趣可以看我另外一篇介绍CAS的文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
volatile底层的实现原理是基于内存屏障(Memory Barrier)的机制。在每个volatile写操作的前面会插入一个StoreStore屏障,它保证volatile写操作之前,所有普通写操作的结果都会被刷新到主内存中,以便对其他线程可见。而在每个volatile写操作的后面会插入一个StoreLoad屏障,它的作用是避免volatile写操作与后面可能有的volatile读/写操作重排序。 另外,在每个volatile读操作的后面会插入一个LoadLoad屏障,它用来禁止编译器将volatile读操作与下面的普通读写操作进行重排序。同时,在每个volatile读操作的后面会插入一个LoadStore屏障,它也是为了确保有序。 总体来说,volatile关键字只能保证可见和有序,但无法保证原子。如果需要保证原子,可以使用Synchronized等锁机制来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [volatile底层实现原理](https://blog.csdn.net/qq_40714246/article/details/118966064)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [volatile的底层原理与实现](https://blog.csdn.net/u022812849/article/details/109257860)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值