主要讲解volatile的相关知识,以及容易遇到的坑。
保证可见性,不保证原子性:
禁止指令重排,我们回顾一下,重排序需要遵守一定规则:
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
该部分相关内容,我直接copy上一篇文章,不是为了凑篇幅,因为有同学没有看上一篇文章,直接看这篇,为了能让每一篇文章能独立成章,可能会引用之前文章中的内容。
先看下面未使用volatile的代码:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
因为重排序影响,所以最终的输出可能是0,,具体分析请参考我的上一篇文章《Java并发编程系列1-基础知识》,如果引入volatile,我们再看一下代码:
class ReorderExample {
int a = 0;
boolean volatile flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
这个时候,volatile禁止指令重排序也有一些规则,这个过程建立的happens before关系可以分为两类:
上述happens before关系的图形化表现形式如下:
在上图中,每一个箭头链接的两个节点,代表了一个happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens before保证。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
下面是变量自加的示例:
public class volatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final volatileTest test = new volatileTest();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("inc output:" + test.inc);
}
}
测试输出:
inc output:8182
因为inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到10000。
采用synchronized:
public class volatileTest1 {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final volatileTest1 test = new volatileTest1();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add synchronized, inc output:" + test.inc);
}
}
采用Lock:
public class volatileTest2 {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
inc++;
lock.unlock();
}
public static void main(String[] args) {
final volatileTest2 test = new volatileTest2();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add lock, inc output:" + test.inc);
}
}
采用AtomicInteger:
public class volatileTest3 {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
public static void main(String[] args) {
final volatileTest3 test = new volatileTest3();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<100;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add AtomicInteger, inc output:" + test.inc);
}
}
三者输出都是1000,如下:
add synchronized, inc output:1000
add lock, inc output:1000
add AtomicInteger, inc output:1000
先看一下单例代码:
public class penguin {
private static volatile penguin m_penguin = null;
// 避免通过new初始化对象
private void penguin() {}
public void beating() {
System.out.println("打豆豆");
};
public static penguin getInstance() { //1
if (null == m_penguin) { //2
synchronized(penguin.class) { //3
if (null == m_penguin) { //4
m_penguin = new penguin(); //5
}
}
}
return m_penguin; //6
}
}
在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码:
a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:
最后也讲解了volatile不适用的场景,以及解决的方法,并解释了单例模式为何需要使用volatile。
欢迎大家多多点赞,更多文章,请关注微信公众号“楼仔进阶之路”,点关注,不迷路~~