Volitale关键字和单例模式双重检测锁
volatile:
volatile关键字是在多线程环境下我们用来保证变量的可见性以及有序性用来修饰该变量的关键字
先来看一段代码,这段代码中我们希望A线程能够第一时间读取到变量value的改变并且B线程能够去输出当前value的最新值
public class TestDemo10 {
public static int value=0;
public static void main(String[] args) {
new Thread("A") {
@Override
public void run() {
int localValue = value;
while (localValue < 5) {
if (localValue != value) {
System.out.println("the value is updated to " + value);
localValue = value;
}
}
}
}.start();
new Thread("B") {
@Override
public void run() {
int localValue = value;
while (localValue < 5) {
System.out.println("the value will be changed to " + (++localValue));
value = localValue;
//短暂睡眠,使得A线程进行输出
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
但是我们发现运行结果并不是我们想象中的那样
结果显示B线程对变量做的修改并不能及时同步到A线程中,让我们对变量value加上Volatile关键字再看结果
public static volatile int value=0;
其余代码不变我们只在value变量上加上volatile关键字
再看运行结果
可以看到A线程第一时间读取到了线程B对value变量做的修改,那volatile关键字是如何做到的呢,首先来看正常情况下两个线程是如何操作value变量的
对于两个线程A和B,其实都是拿主内存中的value副本进行操作,并且在线程工作结束后才会将最新值刷新到主内存中,所以对于变量value的修改与读取与另一个线程并无交互,但是当我们使用volatile修饰value变量之后,如果线程A或者B对value进行了修改,会强制刷新到主内存中,而进行读取的线程拿到的value将会失效,必须从主内存中重新读取最新的value来工作,也就是这样来保证该变量的可见性。
同时,volatile关键字也可以禁止指令重排序,保证代码的有序性,但是需要注意的是,volatile关键字不能保证原子性,如果一个操作比如i++操作本身就不是原子操作,那么你给i加上volatile关键字,任然无法保证其原子性,可以通过下面这段代码来验证**
我们希望每个线程对变量sum进行1000次++操作,同时希望十个线程得到的值是10000
public class Base
{
public static int sum=0;
public volatile static void print(){
sum++;
}
public static void main(String[] args) {
for (int i=0;i<10;i++){
new Thread(){
@Override
public void run() {
for (int j=0;j<1000;j++){
print();
}
}
}.start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(sum);
}}
但是多次输出结果都是小于10000,其原因就在于sum++并不是一个原子性操作,即使我们使用了volatile关键字,如果想保证原子性可以将print()方法使用synchronized关键字修饰即可
使用场景:最常用的场景在于单例模式的双重检测锁,对于单例模式不了解的读者可以点击下方连接:
JAVA单列模式
双重检测锁主要针对单例模式中的懒汉模式
正常的懒汉模式在单线程环境下并不会出现问题,但是在多线程环境下可能出现一个类有两个实例的情况,这样就不满足单例模式的要求了,所以我们加上synchronize关键字保证只有一个实例
class Sigledemo {
private static Sigledemo sigledemo=null;
public static final Object lock=new Object();
private Sigledemo(){
}
public static Sigledemo getInstance(){
synchronized (lock){
if (sigledemo==null){
sigledemo=new Sigledemo();
}}
return sigledemo;
}
}
但是这样会出现的问题是什么,如果多个线程同时需要使用这个方法,并且已经有线程创建了实例,就会出现频繁的加锁解锁现象,造成各种资源的浪费,我们对这个代码进行修改,进行两次判断
public static Sigledemo getInstance(){
if (sigledemo==null){
synchronized (lock){
if (sigledemo==null)
sigledemo=new Sigledemo();
}}
return sigledemo;
}
使用两次判读即可以保证不会有多次加锁解锁现象,也不会出现两个实例的问题,我们一直在用synchronize关键字,这个代码看起来也没有问题,但是如果是下面这样呢?
class Sigledemo {
private static Sigledemo sigledemo=null;
public static final Object lock=new Object();
public A a;
public B b;
private Sigledemo(){
a=new A();
b=new B();
}
public static Sigledemo getInstance(){
if (sigledemo==null){
synchronized (lock){
if (sigledemo==null)
sigledemo=new Sigledemo();
}}
return sigledemo;
}
我们想通过单例的实例去访问A和B的实例,这个时候就会出现问题
A线程先获取锁去实例化了sigledmo实例,但是还没来得及对a和b进行初始化,B线程获取了锁,发现实例不为空,认为a和b已经被初始化,去调用了a或者b,结果出现了空指针异常,针对这种问题,我们就可以在这个sigledmo实例上加上volatile关键字,就可以解决这个问题
使用volatile关键字注意点:
1.volatile关键字只能修饰基本数据类型
2.volatile修饰引用只能保证这个引用的可见性,并不能保证这个引用的指向的可见性
3.虽然在某些地方volatile不如synchronized好用,但是在各种开销上,都要比synchronize小一些,所以在可以使用volatile情况下尽量去使用减少开销