java多线程之内存可见性

来源:互联网 发布:js sleep 3秒 编辑:程序博客网 时间:2024/06/06 08:50

1.什么是可见性?

可见性: 一个线程对共享变量的修改 能够及时被其他线程看到。

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

2.java的内存模型

Java内存模型(java memery model)描述了java程序中 各种变量(线程共享变量)的访问规则,以及在jvm中将变量存储到内存 和 从内存读取出变量这样的底层细节。

l -- 所有的变量都存储在主内存中

l -- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中改变量的一份拷贝)

2.1两条规则

l -- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写

l -- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

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

线程1对共享变量的修改要想被线程2 及时看到,必须要经过如下两个步骤:

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

B. 将主内存中最新的共享变量 的 值更新到工作内存2中

2.3可见性分析

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

A.      线程的交叉执行

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

C.      共享变量更新后的值没有在工作内存和 主内存中 及时更新


2.4共享变量相关知识点

2.4.1指令重排序

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

主要分为3中重排序:

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

B.      指令级并行的重排序(处理器优化)

C.      内存系统的重排序(处理器优化)

2.4.2 as-if-serial

  as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义),但是在多线程程序交错执行时,重排序可能会造成内存可见性的问题

3.Java语言层面支持的可见性的实现方式

3.1 synchronized实现可见性

Synchronized能够实现:

l -- 原子性(同步)

l -- 可见性

JVM关于synchronized的两条规定:

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

l -- 线程加锁时,将清空 工作内存中的 共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

l -- Ps:加锁和解锁 需要是同一把锁。线程解锁前对共享变量的修改在下次加锁时对其他线程可见

 

线程执行互斥代码的过程:

1.      获得互斥锁

2.      清空工作内存

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

4.      执行代码

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

6.      释放互斥锁

 

实现可见性的解决方案:

1.      对于线程交叉执行造成的可见性问题,sychronized的原子性可以保证当前锁对象只有一条线程在执行(sychronize的原子性)

2.      对于重排序结合线程交叉执行造成的可见性问题,使用syncronized后可以不用担心了,因为只有一条线程,根据as-if-serial规则,即使重排序也不会对当前执行结果造成影响(sychronize的原子性)

3.      对于变量未及时更新造成的可见性问题(synchronized的可见性,详细见上文)

public class SynchronizedDemo {//共享变量private boolean ready = false;private int number = 1;private int result = 0;//写操作public void write () {ready = true;//1.1number = 2;        //1.2}//读操作public void read () {if (ready) {//2.1result = number * 3;    //2.2}System.out.println("result值为:" + result);}//内部线程类private class ReadWriteThread extends Thread {private boolean flag;//构造方法中传入的flag参数,确定线程执行写操作还是读操作public ReadWriteThread(boolean flag) {this.flag = flag;}@Overridepublic void run() {if (flag) {write();} else {read();}}}public static void main(String[] args) {SynchronizedDemo demo = new SynchronizedDemo();demo.new ReadWriteThread(true).start();demo.new ReadWriteThread(false).start();}}

执行上面的代码,结果可能是多种。

分析一下,如果按照我们预期的执行,应该是在main方法中传入true的逻辑执行完毕在执行传入false的逻辑,这样执行的逻辑应该是

1.1->1.2->2.1->2.2,最后的结果是6.

但还有很多种情况,比如1.1->2.1->2.2->1.2,这时的结果是3....

这是因为两条线程同时访问,我们不能够保证读写发生顺序 以及 在读写内部因为重排序 而 导致的变量可见性问题

解决办法:synchronized关键字

//写操作public synchronized void write () {ready = true;//1.1number = 2;//1.2}//读操作public synchronized void read () {if (ready) {//2.1result = number * 3;//2.2}System.out.println("result值为:" + result);}

这样我们在保证了读写的原子性之后,同时只能有一条线程进行读写操作了,当然此处因为两条线程基本上同时启动,所以读操作也可能发生在写操作之前,造成结果为0的情况。

3.2Volatile实现可见性

Volatile关键字

l -- 能够保证volatile变量的可见性

l -- 不能保证volatile变量复合操作的原子性

 

深入来说:volatile实现可见性是通过加入内存屏障和 禁止 重排序优化来实现的:

l -- 对volatile变量执行写操作时,会在操作后加入一条store屏障指令,强制将工作内存中的共享变量刷新到主内存中。同时还能防止处理器将 volatile变量之前的操作放到volatile写操作之后。

l -- 对volatile变量执行读操作的时候,会在读操作前加入一条load屏障指令,也有禁止重排序的效果。

 

Volatile如何实现内存的可见性:

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

Volatile不能保证volatile变量复合操作的原子性,如下代码所示:

public class VolatileDemo {private volatile int number = 0;public int getNumber() {return this.number;}public void increase () {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.number++;}public static void main(String[] args) {final VolatileDemo demo = new VolatileDemo();for (int i = 0; i < 500 ; i++) {new Thread(new Runnable () {@Overridepublic void run() {demo.increase();}}).start();}//如果还有子线程在运行,主线程加让出cpu资源//直到所有的子线程都运行完了,主线程再继续往下执行while (Thread.activeCount() > 1) {Thread.yield();}System.out.println("number:" + demo.getNumber());}}

这时运算的结果可能会小于500

因为increase()方法中的number++;方法在执行的时候大致分为三部分,从共享内存中取值,更改值,将更改后的值刷新到公共内存。

而volatile关键字却不能够保证这三部的原子性。

解决办法:

public void increase () {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (this) {this.number++;}}


Volatile适用场景:

要在多线程中安全的使用volatile变量,必须同时满足:

l -- 对变量的写入操作不依赖其当前值(volatile所修饰的变量改变后的值 不能 和改变之前的值有关系,比如count++、count = count*5等)

l -- 该变量没有包含在具有其他变量的不变式中。比如:不变式 : low<up

4. synchronized与volatile的比较

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

B. 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁

C. Synchronized既能够保证可见性,又能够保证原子性,而volatile只能保证可见性,不能保证原子性

5.总结

即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存中得到及时的更新?

一般只有在短时间内的高并发的情况下才会出现变量得不到及时更新的情况,因为cpu执行时会很快的刷新内存,所以一般情况下很难看到这个问题


原创粉丝点击