Java多线程之内存可见性

2019-05-20   1,091 次阅读


什么叫“可见性”?

一个线程对共享变量值的修改,能够被其他线程及时看到。

共享变量:如果一个变量在多个线程的工作内存中存在副本,那么这个变量就是这几个线程的共享变量。

所有变脸都存在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用大的变量副本,关系如下图所示:

 

多线程遵守的两条规定

1.线程对共享变量所有的操作都只能在自己的工作内存中完成,无法直接从主内存中读写

2.不同线程之间无法访问其他线程中的变量,线程中变量值的传递需要通过主内存来完成。

 

共享变量可见性的实现原理

线程1对共享变量的修改如果要被线程2及时看到,需要经过2个步骤:

1.把工作内存1中更新过的共享变量值刷新到主内存中

2.把主内存中最新的共享变量的值更新打工作内存2中

以上2个步骤,任意一个出现问题,都会导致共享变量无法被其他线程及时看到,无法实现可见性,导致其他线程读取的数据不准确从而产生线程不安全。

 

共享变量可见性的实现方式

Java语言层面支持的可见性实现方式有2种,分别是synchronizedvolatile

synchronized:能够实现原子性(同步)和可见性

volatile:能够保证可见性,但是无法保证原子性

 

synchronized是如何实现可见性?

java内存模型(JMM)中关于synchronized的两条规定:

1).线程解锁前,必须把共享变量的最新值刷新到主内存中

2).线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新值(注意:加锁与解锁需要是同一把锁)

 

线程执行互斥代码的过程

1.获得互斥锁

2.清空工作内存

3.从主内存拷贝变量的最新副本到工作内存中

4.执行代码

5.将更改后的共享变量值刷新到主内存中

6.释放互斥锁

 

指令重排序

代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或者处理器为了提高程序性能而做的优化。

目前的指令从排序有3种方式:

1.编译器优化的重排序(编译器优化)

2.处理器优化的重排序(处理器优化)

3.内存优化的重排序(处理器优化)

 

as-if-serial

无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致(java编译器和处理器运行时都会保证在单线程中遵循as-if-serial规则,多线程存在程序交错执行时,则不遵守)

举例:

int num1 = 1;

int num2 = 2;

int num3 = num1 + num2;

上面3行代码,在单线程时,第1、2行可以进行重排序,但是第3行不可以,否则结果将不一样,所以从排序不会给单线程带来内存可见性的问题。

而在多线程中,程序交错执行时,重排序则会造成内存可见性的问题。

 

Synchronized实现可见性的代码,以下的这个类SynchronizedDemo 

public class SynchronizedDemo {
    // 共享变量
    private boolean ready  = false;
    private int     num    = 1;
    private int     result = 0;
</span><span style="color: #008000;">//</span><span style="color: #008000;"> 写操作</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> write() {
    ready </span>= <span style="color: #0000ff;">true</span>; <span style="color: #008000;">//</span><span style="color: #008000;"> 1.1</span>
    num = 2; <span style="color: #008000;">//</span><span style="color: #008000;"> 1.2</span>

}

</span><span style="color: #008000;">//</span><span style="color: #008000;"> 读操作</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> read() {
    </span><span style="color: #0000ff;">if</span> (ready) { <span style="color: #008000;">//</span><span style="color: #008000;"> 2.1</span>
        result = num * 3; <span style="color: #008000;">//</span><span style="color: #008000;"> 2.2</span>

}
System.out.println(
"result = " + result);
}

</span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">class</span> ReadWriteThread <span style="color: #0000ff;">extends</span><span style="color: #000000;"> Thread {
    </span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">boolean</span><span style="color: #000000;"> flag;

    </span><span style="color: #0000ff;">public</span> ReadWriteThread(<span style="color: #0000ff;">boolean</span><span style="color: #000000;"> flag) {
        </span><span style="color: #0000ff;">this</span>.flag =<span style="color: #000000;"> flag;
    }

    @Override
    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
        </span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (flag) {
            write();
        } </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {
            read();
        }
    }
}

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
    SynchronizedDemo synchronizedDemo </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> SynchronizedDemo();
    synchronizedDemo.</span><span style="color: #0000ff;">new</span> ReadWriteThread(<span style="color: #0000ff;">false</span><span style="color: #000000;">).start();
    synchronizedDemo.</span><span style="color: #0000ff;">new</span> ReadWriteThread(<span style="color: #0000ff;">true</span><span style="color: #000000;">).start();
}

}

上面的这一段代码重排序后的执行顺序可能是

1. 1.2-->2.1-->2.2-->1.1;  result=0

2. 1.1-->2.1-->2.2-->1.2;  result=3

......

导致共享变量在线程之间不可见的原因

1.线程的交叉执行

2.重排序结合线程交叉执行

3.共享变量更新后的值,没有在工作内存与主内存间及时刷新

 

安全的代码,加入synchronized关键字

    // 写操作
    public synchronized void write() {
        ready = true; // 1.1
        num = 3; // 1.2
    }
</span><span style="color: #008000;">//</span><span style="color: #008000;"> 读操作</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> read() {
    </span><span style="color: #0000ff;">if</span> (ready) { <span style="color: #008000;">//</span><span style="color: #008000;"> 2.1</span>
        result = num * 2; <span style="color: #008000;">//</span><span style="color: #008000;"> 2.2</span>

}
System.out.println(
"result = " + result);
}

 

volatile是如何实现可见性?

深入来说,是通过加入内存屏障和禁止重排序优化来实现的。

对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,会将cup数据强制刷新到主内存中去

对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,强制缓存器中的缓存失效,每次使用都要去主内存中重新获取数据

通俗地讲,volatile变量在每次被访问的时候,都强迫从主内存中读取该变量的值,而当该变量在发生变化时,又会强迫变量讲最新的值刷新到主内存中,这样,任意时刻,不同的线程总能看到该变量的最新值。

 

线程写volatile变量的过程:

1.改变线程工作内存中volatile变量副本的值

2.将改变的副本的值从工作内存中刷新到主内存中

 

线程读volatile变量的过程:

1.从主内存中读取volatile变量的最新值到工作内存中

2.从工作内存中读取volatile变量的副本

 

volatile不能保证原子性,请看下面的代码:

public class VolatileDemo {
    private volatile int num = 0;
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">int</span><span style="color: #000000;"> getNum() {
    </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">this</span><span style="color: #000000;">.num;
}

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> increase() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> num++,不是原子操作,这里会先读取,再加1</span>
    <span style="color: #0000ff;">this</span>.num++<span style="color: #000000;">;
}

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> main(String[] args) {
    </span><span style="color: #0000ff;">final</span> VolatileDemo volatileDemo = <span style="color: #0000ff;">new</span><span style="color: #000000;"> VolatileDemo();
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 创建500个子线程,执行increase方法,每次都让num加1</span>
    <span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">int</span> i = 0; i &lt; 500; i++<span style="color: #000000;">) {
        </span><span style="color: #0000ff;">new</span> Thread(<span style="color: #0000ff;">new</span><span style="color: #000000;"> Runnable() {
            @Override
            </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
                volatileDemo.increase();
            }
        }).start();
    }
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 等到所有子线程执行完毕,eclipse这里是1,IntelliJ IDEA执行用户代码的时候,实际是通过反射方式去调用,而与此同时会创建一个Monitor Ctrl-Break 用于监控目的,所有是2</span>
    <span style="color: #0000ff;">while</span> (Thread.activeCount() &gt; 2<span style="color: #000000;">) {
        Thread.yield();
    }
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 由于num使用了volatile关键字,所以预期值应该是500</span>
    System.out.println("当前的num值=" +<span style="color: #000000;"> volatileDemo.getNum());
}

}

执行后,会发现,有时候不是500,而是499或者498或者497等等,原因是num++不是原子操作,volatile只能保证变量修改后的可见性,但是无法保证原子性,请看下面的步骤:

假设现在num=5

1.线程A读取num的值,线程A的工作内存中,num=5

2.线程B也读取了num的值,线程B的工作内存中,num=5

3.线程B进行加1操作,线程B的工作内存中,num=6

4.线程B写入最新的num值,主线程中num的值变为6

5.线程A执行加1操作,线程A的工作内存中,num=6

6.线程A写入最新的num值,主线程中num的值变为6

这样,两个线程各自执行了一次加1操作,但是主线程中的数据num=6,这就是由于volatile没办法保证代码的原子性,使得读和写不是一起的

解决方案:

1.使用synchronized关键字

2.使用ReentrantLock

3.使用AtomicInteger

 

volatile的适用场景

1.对变量的写入操作不依赖其当前值

  不满足:num++、count = count * 5

  满足:boolean值变量,记录温度变化的变量等等

2.该变量没有包含在具有其他变量的不变式中

  不满足:low < up

一般的应用场景很多会不满足其中一个,所以volatile是使用没哟synchronized这么广泛。

 

synchronized与volatile比较

1.volatile不需要加锁,比synchronized更轻量级,不会阻塞线程

2.从内存的角度,volatile读操作相当于加锁,写操作相当于解锁

3.synchronized既能保证原子性又能保证可见性,而volatile只能保证可见性无法保证原子性

 

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议