【java】关键字volatile
volatile
1. 含义:
volatile是JVM提供的轻量级的同步机制,具有三个特点:保证可见性、不保证原子性、禁止指令重排。
1.1 保证可见性
一个线程修改了共享变量并写回主内存,其他线程可以自动知道共享变量发生了改变;即共享变量的变化对其他线程可见。这种自动不是指线程自身主动去读主内存的变量,而是由JVM主动告知各线程。通过volatile,解决了工作内存与主内存之间数据同步延迟造成的不可见问题。
验证:
-
1 import java.util.concurrent.TimeUnit; 2 3 class MyData{ 4 volatile int number = 0; //number就是共享变量 5 public void add(){ 6 this.number = 10; 7 } 8 } 9 /* 10 1 验证volatile的可见性 11 1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性 12 1.2 添加了volatile,可以解决可见性问题 13 14 * */ 15 public class VolatileDemo_1 { 16 public static void main(String[] args){ 17 MyData myData = new MyData(); 18 System.out.println("initial:"+myData.number); 19 20 new Thread(()->{ 21 System.out.println(Thread.currentThread().getName()+":start"); 22 try{ TimeUnit.SECONDS.sleep(3);} catch(InterruptedException e) {e.printStackTrace();} 23 myData.add(); 24 System.out.println(Thread.currentThread().getName()+":"+myData.number); 25 System.out.println(Thread.currentThread().getName()+":finish"); 26 },"A").start(); 27 28 while(myData.number == 0){ 29 //System.out.println("loop"); 30 } 31 32 System.out.println(Thread.currentThread().getName()+":"+myData.number); 33 } 34 }
分析:
[ 1 ] 当没有添加volatile关键字,是不可见状态;
- while循环体内为空时:这时候,A是一个线程,经过3秒,A修改了变量number=10,并写回主内存;main是主线程,由于不可见,main无法自动获取number的变化,其值仍然为0,阻塞在while循环。如下图所示。

- while循环体内不为空时(如一个输出语句):同样,A是一个线程,经过3秒,A修改了变量number=10,并写回主内存;main是主线程,会读取主内存里面的值并执行循环体;虽然由于不可见不能自动获取number的变化,但它可以主动去读取主内存中变量的值,当主内存变量值变成10,则主线程会退出循环体。

[ 2 ] 当添加volatile关键字,是可见状态;(即 volatile int number = 0;)
- while循环体内为空时:这时候,A是一个线程,经过3秒,A修改了变量number=10,并写回主内存;main是主线程,由于number有volatile修饰,为可见状态,main可以自动获取number的变化,其值变为10,可以自动退出while循环。这就说明了a线程对变量的修改对其他线程可见。

1.2 不保证原子性
原子性指的是不可分割,完整一体;在程序中,指的是线程执行某个事务时不被分割,事务里的指令要么全部成功,要么全部失败。在并发环境中,如果不能保证原子性,则程序结果可能出错。(不保证原子性,则并发的线程之间互相干扰,影响各种线程的事务的完整执行)。volatile的机制不保证原子性,因此说是轻量级的同步机制。
代码示例:
-
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyData{ volatile int number = 0; //number就是共享变量 public void addPlusPlus(){ number++; //++操作包括三步,从主内存读数据;加;写回主内存 } AtomicInteger atomicInteger = new AtomicInteger(); public void addMyAtommic(){ atomicInteger.getAndIncrement(); } } /* 验证volatile不保证原子性 1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。 需要整体完成,要么同时成功,要么同时失败。 2 volatile不可以保证原子性演示 3 如何解决原子性 *加sync *使用我们的JUC下AtomicInteger * */ public class VolatileDemo { public static void main(String[] args){ MyData myData = new MyData(); for (int i = 1; i <= 10 ; i++) { //建立10个线程 new Thread(()->{ for (int j = 1; j <= 1000 ; j++) { myData.addPlusPlus(); myData.addMyAtommic(); } },String.valueOf(i)).start(); } //需要等待上述10个线程都计算完成后,再用main线程去的最终的结果是多少? while(Thread.activeCount() > 2){ Thread.yield(); //当前面的线程没有执行完,则不执行后面的主线程 } ////结果不为10000,说明不保证原子性; //因为原子性意味着最终一致性,我们的算法逻辑的结果是10000,如果不等于10000,说明线程并发执行有互相干扰,导致原子性被破坏。 System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number); System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.atomicInteger); } }
分析:可能出现两个线程在可见性还没生效时同时把加的结果写回主内存,造成+的操作出现冲突而丢失一些自增操作。
-
结果:
1.3 禁止指令重排
在计算机执行程序时,为了提高性能,编译器和处理器会对指令进行重排。主要有编译器优化重排,指令并行重排、内存优化重排。指令重排时会考虑的是指令之间的依赖性(如:如果B依赖A,则B在A后)。如果是单线程,这些重排不会对结果造成影响;但如果是单线程,指令重排可能会造成程序执行出错。volatile通过禁止指令重排来保证有序性。有序性(以及可见性)是通过内存屏障(memory barrier)实现的。指令的有序性是保证最终数据一致性的基础。
拓展:
1) 内存屏障是一个CPU指令,是底层原语,其主要作用有两个:其一,是保证特定操作的有序性,通过插入内存屏障指令可以使内存屏障前后的指令不会重排序。其二,是保证某些变量的内存可见性,方法是强制cpu缓冲数据的刷出,从而使其他线程可以从内存到读取到特定变量的最新版。
2)单例模式(singleton):单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式常见的有懒汉方式(类加载的时候,不会创建对象,调用时才会创建对象。因此类加载速度快,线程相对不安全)和饿汉方式(类加载的时候,创建对象。 因此类加载速度慢, 线程相对安全)。懒汉模式线程不安全,可以通过synchronized对代码段进行加锁保证线程安全,但限制了高并发。
3)多线程下的单例DCL(双端检锁机制):通过两次判断,一次加锁,从而避免了整个代码段的加锁;但仍然线程不安全,因为有指令重排的存在。可以通过volatile修饰禁止指令重排,从而最终保证了线程安全。

浙公网安备 33010602011771号