Java并发编程(五)—— volatile

volatile

volatile [ˈvɑːlətl] 挥发性的;不稳定的;爆炸性的;反复无常的 synchronized [ˈsɪŋkrənaɪzd] 同步的;同步化的

1 volatile的特性

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

举个例子,定义一个表示程序是否运行的成员变量boolean on = true,那么另一个线程可能对它执行关闭动作on = false,这里涉及多个线程对变量的访问,因此需要将其定义成为volatile boolean on = true,这样其他线程对它进行改变时,可以让所有线程感知到变化,因为所有对on变量的访问和修改都需要以共享内存为准。但是,过多地使用volatile是不必要的,因为它会降低程序执行的效率。

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。 但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操就会有问题。

所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一 致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile的两条实现原则:理器缓存回写到内存;一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

volatile在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度,可以说,volatile是轻量级的synchronized

一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。 代码如下:

class VolatileFeaturesExample { 
  volatile long vl = 0L; // 使用volatile声明64位的long型变量
  
  public void set(long l) { 
    vl = l; // 单个volatile变量的写
  }  
  
  public void getAndIncrement () {
     vl++; // 复合(多个)volatile变量的读/写 
  }
  
  public long get() {
      return vl; // 单个volatile变量的读
	} 
}

在语义上等价于:

class VolatileFeaturesExample { 
  long vl = 0L; // 64位的long型普通变量
  
	public synchronized void set(long l) { 
    vl = l; // 对单个的普通变量的写用同一个锁同步
  }
  
  public void getAndIncrement () {
 	  long temp = get(); // 普通方法调用
    temp += 1L; // 调用已同步的读方法
    set(temp); // 普通写操作
	}   // 调用已同步的写方法                          
   
  public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
  	return vl; 
  }
}

锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。 如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

volatile变量自身具有下列特性:

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

2 volatile写-读的内存语义

JDK 1.5开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果,volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flaga都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

共享变量的状态示意图

如图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

下图为线程B读同一个volatile变量后,共享变量的状态示意图:

共享变量的状态示意图

如图所示,在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致。

如果把volatile写和volatile读两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

volatile写和volatile读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

3 volatile内存语义的实现

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序(有序性)。

下图是volatile重排序规则表:
volatile重排序规则表

从表中可以看出:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

4 volatile VS synchronized

  • volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值