volatile关键字

volatile

  volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。

volatile原理

  volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
  2. 它会强制将对缓存的修改操作立即写入主存
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效

并发编程的3个基本概念

【1】原子性
  即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

  1. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作
  2. 所有引用reference的赋值操作
  3. java.concurrent.Atomic.* 包中所有类的一切操作

【2】可见性
  指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
  内存屏障(memory barrier) 是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
  内存屏障和volatile什么关系?如果字段是volatile,Java内存模型将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。这意味着如果对一个volatile字段进行写操作,必须知道:1、一旦完成写入,任何访问这个字段的线程将会得到最新的值。2、你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
【3】有序性
  即程序执行的顺序按照代码的先后顺序执行。
  Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
  在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

锁的互斥和可见性

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
【1】互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。
【2】可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

Java的内存模型JMM以及共享变量的可见性

  JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
在这里插入图片描述
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存

volatile变量的特性

(1)保证可见性,不保证原子性

  1. 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去
  2. 这个写会操作会导致其他线程中的缓存无效

volatile为什么不能保证原子性?

  volatile从最终汇编语言从面来看,volatile使得每次将i进行了修改之后,增加了一个内存屏障lock addl $0x0,(%rsp)保证修改的值必须刷新到主内存才能进行内存屏障后续的指令操作。但是内存屏障之前的指令并不是原子的。

代码例子:
public static volatile int race = 0;    
public static void increase() {
        race++;    
}
字节码:
public static void increase();    
Code:       
0: getstatic     #2                  // Field race:I       
3: iconst_1       
4: iadd       
5: putstatic     #2                  // Field race:I       
8: return

指令“lock; addl $0,0(%%esp)”表示加锁,把0加到栈顶的内存单元,该指令操作本身无意义,但这些指令起到内存屏障的作用,让前面的指令执行完成。具有XMM2特征的CPU已有内存屏障指令,就直接使用该指令volatile方式的i++,总共是四个步骤:i++实际为load、Increment、store、Memory Barriers 四个操作。内存屏障是线程安全的,但是内存屏障之前的指令并不是.在某一时刻线程1将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。如果在此时线程2也执行同样的操作,获取值i== 10,自增1变为11,然后马上刷入主内存。此时由于线程2修改了i的值,实时的线程1中的i== 10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。

(2)禁止指令重排
  重排序是指在JVM编译时期或者CPU执行JVM字节码时期,对现有的指令进行重排序,主要目的为了优化程序性能。重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果。
  使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
  2. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行

即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行且前面语句的结果对volatile变量及其后面语句可见。

内存屏障的种类

屏障类型作用
LoadLoadBarriers确保 Load1 数据的装载,之前于Load2 及所有后续装载指令的装载
StoreStoreBarriers确保 Store1 数据对其他处理器可见(刷新到内存),之前于Store2 及所有后续存储指令的存储
LoadStoreBarriersLoad1 数据装载,之前于Store2 及所有后续的存储指令刷新到内存
StoreLoadBarriers确保 Store1 数据对其他处理器变得可见(指刷新到内存),之前于Load2 及所有后续装载指令的装载。StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

volatile和synchronized的区别

volatile和synchronized特点

  两者的区别涉及线程安全的两个方面:执行控制内存可见。执行控制的目的是控制代码执行(顺序)及是否可以并发执行。内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
  synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。
  volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。对于volatile关键字,当且仅当满足以下所有条件时可使用:

  1. 对变量的写入操作不依赖变量的当前值或者你能确保只有单个线程更新变量的值
  2. 该变量没有包含在具有其他变量的不变式中

volatile和synchronized的区别

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

volatile的适用场景

状态标志

实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
...
public void shutdown() { 
    shutdownRequested = true; 
}
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。

一次性安全发布

  在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
  这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。

private volatile static Singleton instace;   
public static Singleton getInstance(){   
    //第一次null检查     
    if(instance == null){            
        synchronized(Singleton.class) {    //1     
            //第二次null检查       
            if(instance == null){          //2  
                instance = new Singleton();//3  
            }  
        }           
    }  
    return instance;        

如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。
  考察上述代码中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在Singleton 构造函数体执行之前,变量instance 可能成为非 null 的!
  双重检查锁定是如何被破坏的。假设上述代码执行以下事件序列:

  1. 线程 1 进入 getInstance() 方法
  2. 由于 instance 为 null,线程 1 在 //1 处进入synchronized 块
  3. 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非null
  4. 线程 1 被线程 2 预占
  5. 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将instance 引用返回,返回一个构造完整但部分初始化了的Singleton 对象
  6. 线程 2 被线程 1 预占
  7. 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化

独立观察

  安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
  使用该模式的另一种应用程序就是收集程序的统计信息。如下代码展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用lastUser 引用来发布值,以供程序的其他部分使用。

public class UserManager {
    public volatile String lastUser; //发布的信息
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
} 

“volatile bean” 模式

  volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。
  在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通——即不包含约束!

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}

开销较低的“读-写锁”策略

  如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
  如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    //读操作,没有synchronized,提高性能
    public int getValue() { 
        return value; 
    } 
 
    //写操作,必须synchronized。因为x++不是原子操作
    public synchronized int increment() {
        return value++;
    }

使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作

多个线程同时进行i++操作

使用循环CAS,实现i++原子操作

关于Java并发包的介绍
  Java提供了java.util.concurrent.atomic包来提供线程安全的基本类型包装类。这些包装类都是是用CAS来实现,i++的原子性操作。以AtomicInteger为例子,讲一下 public final int getAndIncrement(){} 方法的实现。

public final int getAndIncrement() {
        for (;; ) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

使用循环CAS,来实现i++的原子性操作

public class AtomicIntegerTest {
	private static AtomicInteger count = new AtomicInteger(0);
	private static final int times = 10000;
	AtomicInteger atomicInteger;
	public static void main(String[] args) {
		long curTime = System.nanoTime();
		Thread decThread = new DecThread();
		decThread.start();
		System.out.println("Start thread: " + Thread.currentThread() + " i++");
		for (int i = 0; i < times; i++) {
			// 进行自加的操作
			count.getAndIncrement();
		}
		System.out.println("End thread: " + Thread.currentThread() + " i++");
		// 等待decThread结束
		while (decThread.isAlive())
			;
		long duration = System.nanoTime() - curTime;
		System.out.println("Result: " + count);
		System.out.format("Duration: %.2fs\n", duration / 1.0e9);
	}
	private static class DecThread extends Thread {
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 进行自减的操作
				count.getAndDecrement();
			}
			System.out
					.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

使用锁机制,实现i++原子操作

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
	private static int count = 0;
	private static final int times = 10000;
	// 使用Lock实现,多线程的数据同步
	public static ReentrantLock lock = new ReentrantLock();
	public static void main(String[] args) {
		long curTime = System.nanoTime();
		Thread decThread = new DecThread();
		decThread.start();
		System.out.println("Start thread: " + Thread.currentThread() + " i++");
		for (int i = 0; i < times; i++) {
			// 进行自加的操作
			try {
				lock.lock();
				count++;
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}
		System.out.println("End thread: " + Thread.currentThread() + " i++");
		// 等待decThread结束
		while (decThread.isAlive())
			;
		long duration = System.nanoTime() - curTime;
		System.out.println("Result: " + count);
		System.out.format("Duration: %.2fs\n", duration / 1.0e9);
	}
	private static class DecThread extends Thread {
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 进行自减的操作
				try {
					lock.lock();
					count--;
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
			System.out
					.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

使用synchronized,实现i++原子操作

public class SynchronizedTest {
	private static int count = 0;
	private static final int times = 1000000;
	public static void main(String[] args) {
		long curTime = System.nanoTime();
		Thread decThread = new DecThread();
		decThread.start();
		System.out.println("Start thread: " + Thread.currentThread() + " i++");
		for (int i = 0; i < times; i++) {
			// 进行自加的操作
			synchronized (SynchronizedTest.class) {
				count++;
			}
		}
		System.out.println("End thread: " + Thread.currentThread() + " i++");
		// 等待decThread结束
		while (decThread.isAlive())
			;
		long duration = System.nanoTime() - curTime;
		System.out.println("Result: " + count);
		System.out.format("Duration: %.2fs\n", duration / 1.0e9);
	}
	private static class DecThread extends Thread {
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 进行自减的操作
				synchronized (SynchronizedTest.class) {
					count--;
				}
			}
			System.out
					.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

java中实现可见性的三个关键字

1、volatile

通过内存屏障和禁止指令重排序来保证可见性的。
(a)对volatile进行读操作,会在读操作之前增加一个load屏障指令
(b)对volatile进行写操作,会在写操作之后增加一个store屏障指令
内存屏障:处理器的一组指令,用于实现对内存操作的顺序限制(指令重排时不能把后面的指令重排列到内存屏障之前的位置)

2、synchronized
同步块的可见性是由:对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store、write操作)

3、final
其可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final字段的值

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值