《java并发》--volatile修饰符

来源:互联网 发布:wow7.0装备数据库 编辑:程序博客网 时间:2024/05/17 22:21

volatile

参考:
Java并发:volatile内存可见性和指令重排
你真的了解volatile吗,关于volatile的那些事
java中volatile关键字的含义

  • volatile
    • volatile 作用
    • volatile 理解
      • 代码解读可见性
      • 代码解读无法实现原子性
      • volatile修饰避免代码重排
    • 总结

volatile 作用

  • 保证内存可见性
  • 防止指令重排
  • 不能解决原子性

volatile 理解

java中多线程共享的变量存储在主内存中,每个线程都有自己的工作内存,工作内存保存了主内存的副本,线程要操作共享变量,实际操作的是线程工作内存的副本,操作完毕后再同步写入主内存,各个线程线程只能访问自己的工作内存,不可以访问其它线程的工作内存。

java中线程工作内存跟主内存的交互

image

  1. lock:将主内存中的变量锁定,为一个线程所独占
  2. unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量
  3. read:将主内存中的变量值读到工作内存当中
  4. load:将read读取的值保存到工作内存中的变量副本中。
  5. use:将值传递给线程的代码执行引擎(多次)
  6. assign:将执行引擎处理返回的值重新赋值给变量副本(多次)
  7. store:将变量副本的值存储到主内存中。
  8. write:将store存储的值写入到主内存的共享变量当中。

可见性:保证线程使用共享变量时每次都去主内存获取最新的,保证了read-load的一致性

原子性:保证线程在read-load-use-assign-store-write共享变量过程中,其它线程不能对共享变量进行修改

共享变量使用volatile修饰后,保证线程每次访问共享变量都去主内存获取,保证每次获取到的是主内存中最新的值,即保证了read-load是最新的,这样就实现了可见性,但是在后续的use-assign-store-write过程中,其它线程可能会对共享变量进行操作更改,这样无法保证原子性

代码解读可见性

如果不使用volatile修饰共享变量,线程只会在第一次使用共享变量的时候去主内存加载建立副本,这样子线程永远不会停止

使用volatile修饰修饰共享变量,在while循环的判断running值的时候,每次都去主内存获取最新的值,当主线程将running设置为false的时候,停止子线程,在while循环中使用了count变量,如果只将count用volatile修饰,也能停止子线程,由此可见,线程去主内存读取共享变量的时候,会把所有用到的共享变量都在工作内存建立副本

public class Task implements Runnable{  //将count用volatile修饰,保证每次去主存读取count值,  //读取的同时会将running也从主存读取,不管running是否用volatile修饰  private volatile int count = 0;  private boolean running = true;  @Override  public void run() {    while(running){      //      count++;    }    System.out.println("子线程"+Thread.currentThread().getName()+"停止");  }  public static void main(String[] args) throws InterruptedException {    Task task = new Task();    //启动子线程    new Thread(task).start();    Thread.currentThread().sleep(3000);    task.setRunning(false);    System.out.println("主线程停止");  }  public void setRunning(boolean running) {    this.running = running;  }  public int getCount() {    return count;  }}

代码解读无法实现原子性

下面这段程序执行完毕后无法保证count的数量最终为1000,这是因为volatile只能保证使用count的时候去主内存读取到最新的值,但是在对count进行+1操作的时候,其它线程可能会对count进行修改+1然后写会主内存,造成最后的结果不是1000,如果要保证1000,还是要对整个read到write回主内存保证一致性,这就需要使用synchronized或者lock去实现了。

public class Counter {  //使用volatile修饰共享变量  public volatile static int count = 0;  public static void inc() {    // 这里延迟1毫秒,使得结果明显    try {      Thread.sleep(1);    } catch (InterruptedException e) {    }    //无法保证是1000    count++;  }  public static void main(String[] args) {    // 同时启动1000个线程,去进行i++计算    for (int i = 0; i < 1000; i++) {      new Thread(new Runnable() {        @Override        public void run() {          Counter.inc();        }      }).start();    }    // 无法保证count值为1000    System.out.println("运行结果:Counter.count=" + Counter.count);  }}

volatile修饰避免代码重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序员带来问题。

内存屏障:在使用volatile修饰的变量前后插入一个内存栅栏,告诉JVM该条指令不能跟前后语句进行重排。

指令重排在多线程操作的时候,如果变量没有使用volatile修饰,可能会出现问题

//线程1初始化UserUser user;user = new User();//线程2读取userif(user!=null){  user.getName();}

具体来看User user = new User的语义:
1:分配对象的内存空间
2:初始化对线
3:设置user指向刚分配的内存地址

操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,优化后变为 1->3->2
这些线程1在执行完第3步而还没来得及执行完第2步的时候,如果内存刷新到了主存,那么线程2将得到一个未初始化完成的对象。

//在线程A中:context = loadContext();inited = true;//在线程B中:while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量   sleep(100);}doSomethingwithconfig(context);//假设线程A中发生了指令重排序:inited = true;context = loadContext();//那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。

volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。懒汉式单例模式就是使用volatile防止创建多个实例对象

总结

  • volatile无法实现原子性,只能实现可见性
  • 当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
  • 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
  • 在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。
  • 当且仅当满足以下所有条件时,才应该使用volatile变量:
    1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
    2. 该变量没有包含在具有其他变量的不变式中,防止影响其他变量??
    3. 防止代码重排
0 0
原创粉丝点击