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初始化User
User user;
user = new User();
//线程2读取user
if(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. 防止代码重排
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值