Android多线程编程之Volatile关键字

Android多线程编程之Volatile关键字

有时候仅仅为了读写一个或者两个实例域就使用同步开销大,而volatile关键字为实例域的同步访问
提供了免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发更新的。

内存模型的概念以及并发编程的三个特性:原子性,可见性,有序性


  1. Java内存模型

java中的堆内存又来存储对象实例,堆内存是被所有的线程共享的运行时内存区域
因此,它存在内存可见性的问题,而局部变量,方法定义的参数则不会在线程之间共享,
这很好理解,线程间的私有变量不会共享,他们不会有内存可见性的问题,也不受
内存模型的影响,java内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量
存放在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的
变量副本,需要注意的是本地内存是Java内存模型的一个抽象概念,其实并不真实存在,
它涵盖了缓存,写缓冲区,寄存器等区域。Java内存模型控制线程之间的通信,它决定一个
线程对主存共享变量的写入何时对另一个线程可见。

在这里插入图片描述

线程A要与线程B进行通信必须要经历下面两个步骤:
1.线程A把线程A本地内存中更新过的共享变量刷新到主存中
2.线程B去主存中读取线程A之前已经更新过的共享变量

原子性可见性和有序性

  • 原子性:一个语句含有多个操作时就不满足原子性,只有简单的读取和赋值(将数字
    赋值给某个变量)才是原子性操作

  • 可见性:可见性是指线程之间的可见性,一个线程的修改的状态对另外一个线程是可见的,也就是一个线程修改的结果另一个线程马上就能看到,当一个共享变元被volatile修饰时,它会保证被修改的值立即被更新到主存中,所以对其他线程是可见的,当有其他线程需要读取该值时,会去主存中读取该值,而普通的共享变量不能保证可见性,因为普通变量被修改后,并不会立即写入主存,何时被写入主存也是不确定的,当其他线程去主存中读取该值时有可能是修改之前的旧值,这样就无法保证可见性
  • 有序性:Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响
    单线程执行的正确性,但是会影响到多线程并发执行的正确性,这是可以通过volatile关键字来保证有序性,除了volatile也可以通过synchronized和lock来保证有序性,synchronized和lock保证每个时刻只有一个线程执行同步代码,就相当于让线程顺序执行同步代码,从而保证有序性

volatile关键字保证可见性,有序性但不保证原子性(重点会考的)



2. volatile关键字

当一个共享变量被volatile修饰之后,其具备了两个含义

  • 线程修改了变量的值是,变量的新值对其他线程是立即可见的
  • 禁止编译器或者运行环境为了优化程序性能而采取对指令进行重新排序

重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境

public class VolatikeDemo {
    public volatile int inc=0;
    public void increse(){
        inc++;
    }

    public static void main(String[] args){
        final VolatikeDemo demo=new VolatikeDemo();
        for(int i=0;i<10;i++){
            new Thread(){
                @Override
                public void run() {
                   for(int j=0;j<1000;j++){
                       demo.increse();
                   }
                }
            }.start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(demo.inc);
    }
}

分析:这段代码每次执行的结果都不一样,自增操作不具备原子性,它包括读取变量的原始值,进行加一,写入工作内存也就是说,自增操作的三个子操作可能会分割开执行。
假如某个时刻inc的值是9,某个线程A对变量进行自增操作,先读取了inc的值,然后线程A被阻塞了,线程B也对变量进行自增操作,读取了变量inc的原始值,进行加一操作,并把inc=10写入工作内存,线程A接着对inc进行加一操作,因为已经读了inc的值是9,所以他还会把inc=10写入内存
volatile也无法保证对变量的操作时原子性的

volatile保证有序性
volatile能禁止指令重排序,因此能保证有序性




  1. 正确使用volatile关键字

synchronized关键字可以防止多个线程同时执行一段代码,这对执行效率有很大的
影响,而volatile关键字在某些情况下的性能优于synchronized,但是volatile是无法
替代synchronized关键字的,因为volatile是无法保证原子性的,通常来说,使用volatile
必须具备下面两个条件

  • 对变量的写操作不会依赖当前的值(不能自增自减)
  • 该变量没有包含在其他变量的不变式中。

使用volatile有很多种场景,这就介绍其中的两种
1.状态标志

public class VolatikeDemo {
   public volatile boolean on;
   public void shutdown(){
       on=true;
   }
   public void dowork(){
       while (!on){
           System.out.println("on=false");
       }
   }
   public static  void main(String args[]){
       final VolatikeDemo demo=new VolatikeDemo();
      new Thread(){
          @Override
          public void run() {
              demo.dowork();
          }
      }.start();
       new Thread(){
           @Override
           public void run() {
               demo.shutdown();
           }
       }.start();
   }
}

上面通过两个线程来同时执行on,每个线程都能及时的得知on的值

2.双重检查模式(DCL)

public class VolatikeDemo {
   private volatile static VolatikeDemo instance=null;
   public static VolatikeDemo getInstance(){
       if (instance!=null){
           synchronized (VolatikeDemo.class){
               if(instance==null){
                   instance=new VolatikeDemo()
               }
           }
       }
       return instance;
   }
}


getInstance方法对VolatikeDemo进行了两次判空,第一次是为了不必要的同步,第二次是只有
当VolatikeDemo等于null的情况下才创建实例。DCL的优点是资源利用率高,第一次执行
getInstance方法是单例对象才会被实例化

volatile与synchronized对比
与锁相比,volatile变量是一种非常简单但同时又非常脆弱的同步机制,他在某些情况下
将提供优于锁的性能和伸缩性,如果严格遵循volatile的使用条件,即变量真正独立于
其他变量和自己以前的值,在某些情况下可以使用volatile代替synchronized简化代码。





关于volatile关键字的相关知识已梳理完毕,下一篇博客梳理阻塞队列

发布了123 篇原创文章 · 获赞 74 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43927892/article/details/101079343