5个案例和流程图让你从0到1搞懂volatile关键字

09/20 22:42

volatile

有序性

    static int a, b, x, y;

public static void main(String[] args){
long count = 0;
while (true) {
count++;
a = 0;b = 0;x = 0;y = 0;
a = 1;
x = b;
});
b = 1;
y = a;
});

try {
} catch (Exception e) {}

if (x == 0 && y == 0) {
break;
}
}
//count=118960,x=0,y=0
System.out.println("count=" + count + ",x=" + x + ",y=" + y);
}


//线程1
a = 1;
x = b;

//线程2
b = 1;
y = a;


//线程1
//1           2           3           4
a = 1;      a = 1;      x = b;      x = b;
x = b;      x = b;      a = 1;      a = 1;

//线程2
//1           2           3           4
b = 1;      y = a;      b = 1;      y = a;
y = a;      b = 1;      y = a;      b = 1;


//线程1
a = 1;
unsafe.fullFence();
x = b;

//线程2
b = 1;
unsafe.fullFence();
y = a;


可见性

    //nonVolatileNumber 是未被volatile修饰的
while (nonVolatileNumber == 0) {

}
}).start();

TimeUnit.SECONDS.sleep(1);
nonVolatileNumber = 100;


原子性

        private volatile int num = 0;

public static void main(String[] args) throws InterruptedException {
C_VolatileAndAtomic test = new C_VolatileAndAtomic();
});

});

t1.start();
t2.start();

t1.join();
t2.join();

//13710
System.out.println(test.num);
}

/**
* 循环自增一万次
*
* @param test
*/
private static void forAdd(C_VolatileAndAtomic test) {
for (int i = 0; i < 10000; i++) {
test.num++;
}
}


synchronized加锁的方式就可以保证原子性，因为拿到锁同一时间只有它能进行访问

volatile原理

JVM会对使用volatile修饰的变量，加上volatile的访问标识，在字节码指令运行时会使用操作系统的内存屏障来禁止指令进行重排序

volatile汇编指令实现实际上是lock前缀指令

lock前缀指令在单核下没啥影响，因为单核可以保证有序性、可见性以及原子性

lock前缀指令在多核下会在修改数据时，将其写回内存中，写回内存需要确保同时只有一个处理器操作，可以通过锁总线的方式，但其他处理器就不能访问

伪共享问题

        @sun.misc.Contended
private volatile int i1 = 0;
@sun.misc.Contended
private volatile int i2 = 0;

public static void main(String[] args) throws InterruptedException {
D_VolatileAndFalseSharding test = new D_VolatileAndFalseSharding();
int count = 1_000_000_000;
for (int i = 0; i < count; i++) {
test.i1++;
}
});

for (int i = 0; i < count; i++) {
test.i2++;
}
});

long start = System.currentTimeMillis();

t1.start();
t2.start();

t1.join();
t2.join();

//31910 i1:1000000000 i2:1000000000

//使用@sun.misc.Contended解决伪共享问题  需要携带JVM参数 -XX:-RestrictContended
//5961 i1:1000000000 i2:1000000000
System.out.println((System.currentTimeMillis() - start) + " i1:"+ test.i1 + " i2:"+ test.i2);
}


volatile的使用场景

volatile通过内存屏障来禁止指令重排序，从而保证可见性与有序性

//1.分配内存
//2.初始化对象
//3.将对象指向分配的空间


        private static volatile Singleton singleton;

public static Singleton getSingleton(){
if (Objects.isNull(singleton)){
//有可能很多线程阻塞到拿锁,拿完锁再判断一次
synchronized (Singleton.class){
if (Objects.isNull(singleton)){
singleton = new Singleton();
}
}
}

return singleton;
}


总结

volatile通过内存屏障禁止指令重排序以达到满足有序性和可见性，但不能满足原子性

volatile底层汇编使用lock前缀指令实现，在多核下在修改数据时会锁总线将数据写回内存中，由于锁总线开销大，后续使用锁缓存行，同时使用缓存一致性协议保证同时只能一个处理器修改同一缓存行，通过嗅探技术让其他拥有该缓存行的处理器感知到缓存行变脏，后续重新读取

volatile基于可见性的特点，常在并发编程中实现无锁的读操作；基于有序性的特点，可以在双重检测锁中保证获取到的实例不是还没初始化的

最后（不要白嫖，一键三连求求拉~）

Gitee-JavaConcurrentProgramming/src/main/java/A_volatile

Github-JavaConcurrentProgramming/src/main/java/A_volatile

0 评论
0 收藏
0