volatile
volatile 是Java中的一个修饰符,被volatile 修饰的成员变量在每次被线程访问时,都强制从主内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到主内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是 Volatile变量的同步性较差,而且其使用也更容易出错。
锁提供了两种主要特性:互斥(mutual exclusion)和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
并发三大特性
原子性:
定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作!
如:a=10这种基本类型的读取和赋值操作,且赋值必须是数字赋值给变量就是原子性的,变量之间的相互赋值不是原子性操作。
像a++就不是原子性的,a++实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a
可见性:
定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
出现的原因:在多核处理器中,如果多个线程对一个变量进行操作,多个线程被分配到了多个处理器上,当线程要处理变量时,处理器会将变量从主内存中复制到自己的存储器中,处理完成之后再赋值回主内存。这样就会产生一个问题,如果a和b同时对变量c进行修改,而a和b在不同处理器上,是不可见的。那么就会造成a把修改的值赋值给主内存,如何b也把修改的值赋值给了主内存,相当于a的赋值操作被b覆盖了,这样就会产生一些预期之外的问题。
随便说一下synchronized是如何保证其可见性的
synchronized为一段操作进行加锁之后,它就具有了互斥性。当线程要操作被synchronized修饰的操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁,所以它只允许一个线程进行操作。只有当获取到锁的线程完成操作之后,其他线程才可以进行操作,这样就保证了可见性和原子性。
有序性:
定义:即程序执行的顺序按照代码的先后顺序执行。
出现的原因:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序保证结果一致的方法是处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令b必须用到指令a的结果,那么处理器会保证指令a会在指令b之前执行。
咋一看,好像是没什么问题,但在多线程的环境下呢?
看以下的例子
//线程1:
result= getResult(); //语句1
flag = true; //语句2
//线程2:
while(!flag ){
dosomething();
}
dolater();
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行dolater(result)方法,而此时result并没有被初始化,就会导致程序出错。
synchronized锁在同一时刻只能由一个线程获取,当锁被占用后,其他线程只能等待。所以synchronized中读写共享变量时只能“串行”执行,因此synchronized具有有序性。
volatile具有禁止指令重排序的作用,所以具有有序性。
Volatile变量的作用
1、保证了可见性,不保证原子性
读:被volatile 修饰的成员变量在每次被线程访问时,把该线程对应的本地内存置为无效,都强制从主内存中重新读取该成员变量的值。
写:当成员变量发生变化时,会强制线程将变化值回写到主内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
2、禁止进行指令重排序
被volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
volatile 变量的用法
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile变量替代锁。要使 volatile变量提供理想的线程安全,必须同时满足下面两个条件:
● 对变量的写操作不依赖于当前值。
● 该变量没有包含在具有其他变量的不变式中。
第一个条件的限制使 volatile变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile变量不能像 synchronized 那样普遍适用于实现线程安全。
以下是一些volatile变量的用法
将 volatile变量作为状态标志使用,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean shutdownRequested;
...
public void shutdown()
{
shutdownRequested=true;
}
public void doWork()
{
while(!shutdownRequested)
{
//dostuff
}
}
将 volatile变量用于一次性安全发布
public class BackgroundFloobleLoader{
public volatile Flooble theFlooble;
public void initInBackground(){
//dolotsofstuff
theFlooble = newFlooble();
//this is the only write to theFlooble
}
}
public class SomeOtherClass{
public void doWork(){
while(true){
//dosomestuff...
//usetheFlooble,butonlyifitisready
if(floobleLoader.theFlooble!=null)doSomething(floobleLoader.theFlooble);
}
}
}