java内存模型——volatile的可见性

发布时间:   来源:CSDN  

volatile的使用与线程安全关系密切,主要作用是使变量在多个线程间可见,另外也有防止指令重排的作用。


(相关资料图)

比如主内存中有变量a=0,线程1设置a=10,线程2再操作a的时候,是以a=10的基础上进行操作,否则会影响逻辑!

0 1volatile的可见性

要了解volatile的可见性,首先得了解java内存模型:

java内存模型

Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单来说:

1. 所有变量储存在主内存。

2. 每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。

3. 线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。

线程,主内存,工作内存的交互关系如下图所示

如下列代码所示,rt启动之后修改isRunning的值为false,此时while循环不会停止,因为run方法里得不到改变之后的isRunning。

解决:使用volatile修饰isRunning,这样当isRunning的值改变之后,会立即刷新到主内存里,工作内存也能立即获取到新的值

public class RunThread extends Thread {private boolean isRunning = true;  private void setRunning(boolean isRunning){this.isRunning = isRunning;  }    public void run () {System.out.println("进入run方法");    while(isRunning == true){//...    }    System.out.println("线程停止");  }  public static void main(String[] args) {RunThread rt = new RunThread();    rt.start();    try {Thread.sleep(3000);      rt.setRunning(false);      System.out.println("isRunning的值已经被设置成false");      Thread.sleep(1000);      System.out.println(rt.isRunning);    } catch (InterruptedException e) {// TODO Auto-generated catch block      e.printStackTrace();    }  }}

0 2volatile能防止指令重排

如下列代码所示,这是单例模式的双检锁写法

public class SingletonTest {private volatile static SingletonTest instance = null;    private SingletonTest() { }    public static SingletonTest getInstance() {if(instance == null) {synchronized (SingletonTest.class){if(instance == null) {instance = new SingletonTest();  //非原子操作                }            }        }        return instance;    }}

我们看到instance用了volatile修饰,由于 instance = new SingletonTest();可分解为:

1.memory =allocate(); //分配对象的内存空间2.ctorInstance(memory); //初始化对象3.instance =memory; //设置instance指向刚分配的内存地址

操作2依赖1,但是操作3不依赖2,所以有可能出现1,3,2的顺序,当出现这种顺序的时候,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。

而使用volatile修饰instance之后,不会出现乱序的行为!

0 3volatile不保证原子性以及解决方式

1.什么是原子性?

下列语句中,哪些是原子性操作?

x = 10;         //语句1y = x;          //语句2x++;            //语句3x = x + 1;      //语句4

语句1 是直接将数值 10 赋值给 x,也就是说线程执行这个语句的会直接将数值 10 写入到工作内存中;

语句2 实际上包含两个操作,它先要去读取 x 的值,再将 x 的值写入工作内存。虽然,读取 x 的值以及 将 x 的值写入工作内存这两个操作都是原子性操作,但是合起来就不是原子性操作了;

同样的,x++ 和 x = x+1 包括3个操作:读取 x 的值,进行加 1 操作,写入新的值。

只有 语句1 的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作!

2.举例

如下列代码所示,使用volatile修饰的变量count,利用10个线程分别对count进行++操作,而根据上面的表述,++操作不是原子操作!每个线程加1000个数,打印的结果中一定有一个是10000才是对的,但是实际上并不是这样!因为volatile不保证原子性!

public class VolatileNoAtomic extends Thread{private static volatile int count;//  private static AtomicInteger count = new AtomicInteger(0);  private static void addCount(){for (int i = 0; i < 1000; i++) {count ++;//      count.incrementAndGet();    }    System.out.println(count);  }  public void run(){addCount();  }    public static void main(String[] args) {VolatileNoAtomic[] arr = new VolatileNoAtomic[10];    for (int i = 0; i < arr.length; i++) {arr[i] = new VolatileNoAtomic();    }        for (int i = 0; i < arr.length; i++) {arr[i].start();    }  }}

解决:

方法1:使用原子类Atomic类的系列对象,这样既不会阻塞,又能保证原子性!

方法2:使用synchronized修饰addCount方法,这样做的话,线程同步之后会有阻塞,运行时间加长,而且volatile将会失效,不建议这么改

方法3:使用Lock加锁,当然,跟方法2一样的有阻塞

● 架构系列——使用synchronized需要注意什么细节

● 架构系列——线程实现方式以及生命周期的探索

● 架构系列——并发、并行与多线程关系探索

● 架构系列——单体、分布式、集群与冗余的探索

● Java反射:框架设计的灵魂

● 高并发秒杀系统如何设计与优化

● 要准备多少东西去面试---java中高级面试总结(值得收藏)

● 最近的面试有感(7个方面)

● java中的参数传递(只有值传递没有引用传递)

● 38张史上最全的IT工程师技能图谱(高清收藏)

● PLSQL连接本地oracle或远程oracle数据库,实现随意切换(送福利)

● 通过数据泵expdp、impdp方式备份与还原Oracle数据库--值得收藏

● java常见排序算法--选择排序、冒泡排序、插入排序分析与比较

相关文章Related

返回栏目>>