volatile的适用场景

介绍

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)可见性(visibility)

  • 原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。 所谓原子性操作是指不会被线程调度机子打断的操作,这种操作一旦开始,就一直到幸运星结束,中间不会有任何切换(切换线程)。
  • 可见性则更为微妙,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

volatile的使用条件:

volatile变量具有 synchronized 的可见性特性,但是不具备原子性。这就是说线程能够自动发现 volatile 变量的最新值

volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

使用条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果只从单个线程写入,那么可以忽略第一个条件。)

反例

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。

【反例:volatile变量不能用于约束条件中】 下面是一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

public class NumberRange {  private volatile int lower;private volatile int upper;  public int getLower() { return lower; }  public int getUpper() { return upper; }  public void setLower(int value) {   if (value > upper)   throw new IllegalArgumentException(...);  lower = value;  }  public void setUpper(int value) {   if (value < lower)   throw new IllegalArgumentException(...);  upper = value;  }  
}

lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;而仍然需要使用同步——使 setLower()setUpper() 操作原子化。

否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLowersetUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是(0, 5),同一时间内,线程 A 调用setLower(4) 并且线程 B 调用setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4, 3) —— 一个无效值。

volatile的适用场景

模式 #1:状态标志

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;  ...  public void shutdown() {   shutdownRequested = true;   
}  public void doWork() {   while (!shutdownRequested) {   // do stuff  
    }  
}

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。

而如果使用 synchronized 块编写循环要比使用 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。

这种类型的状态标记的一个公共特性是:通常只有一种状态转换shutdownRequested 标志从false 转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从falsetrue,再转换到false)。此外,还需要某些原子状态转换机制,例如原子变量。

模式 #2:一次性安全发布(one-time safe publication)

在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。

这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。如下面介绍的单例模式。

private  static Singleton instace;     public static Singleton getInstance(){     //第一次null检查       if(instance == null){              synchronized(Singleton.class) {    //1       //第二次null检查         if(instance == null){          //2    instance = new Singleton();//3    
            }    }             }    return instance;   
}

模式 #3:独立观察(independent observation)

安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。【例如】假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

使用该模式的另一种应用程序就是收集程序的统计信息。

【例】如下代码展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用lastUser 引用来发布值,以供程序的其他部分使用。(主要利用了volatile的可见性)

public class UserManager {  public volatile String lastUser; //发布的信息  public boolean authenticate(String user, String password) {  boolean valid = passwordIsValid(user, password);  if (valid) {  User u = new User();  activeUsers.add(u);  lastUser = user;  }  return valid;  }  
}
模式 #4:“volatile bean” 模式

volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通——即不包含约束!

public class Person {  private volatile String firstName;  private volatile String lastName;  private volatile int age;  public String getFirstName() { return firstName; }  public String getLastName() { return lastName; }  public int getAge() { return age; }  public void setFirstName(String firstName) {   this.firstName = firstName;  }  public void setLastName(String lastName) {   this.lastName = lastName;  }  public void setAge(int age) {   this.age = age;  }  
}
模式 #5:开销较低的“读-写锁”策略

如果读操作远远超过写操作,您可以结合使用内部锁volatile 变量来减少公共代码路径的开销。

如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

public class CheesyCounter {  // Employs the cheap read-write lock trick  // All mutative operations MUST be done with the 'this' lock held  @GuardedBy("this") private volatile int value;  //读操作,没有synchronized,提高性能  public int getValue() {   return value;   }   //写操作,必须synchronized。因为x++不是原子操作  public synchronized int increment() {  return value++;  }  
}

使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作。

单例模式

定义:

确保某个类只有一个实例,并提供一个全局访问点。

类图:

1364026153_1078.gif

public class Singleton{  private static final Singleton instance;  private Singleton(){  }  public static Singleton getInstance(){      if(instance == null){          //1  instance = new Singleton();//2  
        }  return instance;               //3  
   }  ...  
}

优点:

  1. 内存中只有一个对象,减少内存开支;
  2. 单例可避免对资源的多重占用,例如写文件动作,可避免对同一资源文件的同时写操作。

缺点:

  1. 单例模式一般没有接口,扩展很困难; ——单例并不是用来继承的。
  2. 不利于测试,并行开发时,若单例未完成,则不能进行测试;
  3. 与单一职责原则冲突,把“要单例”和业务逻辑融合在一个类中。

使用场景:

若出现多个对象就会出现“不良反应”,应该用单例,具体场景如下:

  1. 要求生成唯一序列号的环境;
  2. 在整个项目中需要一个共享访问点或共享数据。例如页面计数器;
  3. 创建一个对象需要消耗的资源过多时;
  4. 需要定义大量的静态常量和静态方法的环境。

为什么不直接用全局变量来实现单例?

有缺点:全局变量必须在程序一开始就创建好。而单例模式可以延迟初始化。

类加载器对单例的影响:

不同的类加载器可能会加载同一个类。

如果程序有多个类加载器,可在单例中指定某个加载器,并指定同一个加载器。

多线程的影响:

上文代码示例在多线程环境下有bug:

  1. 线程 1 调用 getInstance() 方法并决定 instance 在 //1 处为null
  2. 线程 1 进入 if 代码块,但在执行 //2 处的代码行时被线程 2 预占。
  3. 线程 2 调用 getInstance() 方法并在 //1 处决定 instancenull
  4. 线程 2 进入 if 代码块并创建一个新的 Singleton 对象并在 //2 处将变量instance 分配给这个新对象。
  5. 线程 2 在 //3 处返回 Singleton 对象引用。
  6. 线程 2 被线程 1 预占。
  7. 线程 1 在它停止的地方启动,并执行 //2 代码行,这导致创建另一个 Singleton 对象。
  8. 线程 1 在 //3 处返回这个对象。
结果是 getInstance() 方法创建了两个 Singleton 对象。

解决方法一:不用延迟初始化

public class Singleton{  private static final Singleton instance = new Singleton();  private Singleton(){  }  public static Singleton getInstance(){           return instance;  }  ...  
}

解决方法二:同步getInstance

public class Singleton{  private static final Singleton instance;  private Singleton(){  }  //同步getInstance  public static synchronized Singleton getInstance(){      if(instance == null){          //1  instance = new Singleton();//2  
        }  return instance;               //3  
   }  ...  
}

但是synchronized方法会降低性能,尤其这里仅当第一次调用getInstance时才需要同步,只有执行//2代码行时才需要同步。

你可能想到只同步方法块,即只对//2进行同步:

public static Singleton getInstance(){      if(instance == null){            synchronized(Singleton.class) {    instance = new Singleton();  }  }   return instance;  
}

但这样做并不能解决问题:
当 instance 为 null 时,两个线程可以并发地进入if 语句内部。
然后,一个线程进入 synchronized 块来初始化 instance,而另一个线程则被阻断。

    当第一个线程退出 synchronized 块时,等待着的线程进入并创建另一个Singleton 对象。

注意:当第二个线程进入 synchronized 块时,它并没有检查 instance 是否非 null。
还是会创建2个对象。

解决方法三:双重检查加锁

针对上述方法的缺点,我们在//2代码行时 再检查一次null,就能保证只创建一个对象:

 
private  static Singleton instace;   public static Singleton getInstance(){   //第一次null检查     if(instance == null){            synchronized(Singleton.class) {    //1     //第二次null检查       if(instance == null){            //2  instance = new Singleton();//3  
            }  }           }  return instance;
}

假设有下列事件序列:

  1. 线程 1 进入 getInstance() 方法。
  2. 由于 instance 为 null,线程 1 在 //1 处进入synchronized 块。
  3. 线程 1 被线程 2 预占。
  4. 线程 2 进入 getInstance() 方法。
  5. 由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。
  6. 线程 2 被线程 1 预占。
  7. 线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个Singleton 对象并将其引用赋值给instance(由于java执行的无序性,可能赋值时只是占用内存空间(此时instance已经为非null,锁松开,由于无序性,还没有来得及初始化,线程2已经取得instance对象),还没有根据构造函数初始化)。
  8. 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。
  9. 线程 1 被线程 2 预占。
  10. 线程 2 获取 //1 处的锁并检查 instance 是否为 null。
  11. 由于 instance 是非 null 的,并没有创建第二个Singleton 对象,由线程 1 创建的对象被返回,此时返回对象可能是是一个构造完整却没有完全初始化的对象。
  12. 线程1继续执行完成对象的初始化,由于instance是volatile类型的,所以instance变量对所有线程共享可见,所以线程2可以得到一个完整初始化的对象。

对于上面解说的赋值,却没有初始化的原因,是由于java变量重新赋值时有3个步骤的(读取,修改,回写)

代码行 instance =new Singleton(); 执行了下列伪代码

1. mem = allocate();             //Allocate memory for Singleton object.
2. instance = mem;               //Note that instance is now non-null, but//has not been initialized.
3. ctorSingleton(instance);      //Invoke constructor for Singleton passing//instance.

转载于:https://www.cnblogs.com/ouyxy/p/7242563.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/252222.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#如何测试代码运行时间

第一种方式&#xff1a;System.Diagnostics.Stopwatch stopwatch new Stopwatch(); stopwatch.Start(); // 开始监视代码运行时间 // 需要测试的代码 .... stopwatch.Stop(); // 停止监视 TimeSpan timespan stopwatch.Elapsed; // 获取当前实例测量得出的总时间 double …

第二十二章:动画(六)

复合动画您可以混合等待和未等待的调用来创建复合动画。 例如&#xff0c;假设您希望按钮在大小扩展的同时旋转360度然后收缩。ViewExtensions类定义一个方法名称ScaleTo&#xff0c;它为Scale属性设置动画&#xff0c;就像RotateTo为Rotate属性设置动画一样。 Button大小的扩展…

ubantu之Git使用

本文讲述在Ubuntu 14.04 x64环境下&#xff0c;如何安装Git&#xff0c;配置连接GitHub&#xff0c;并且上传本地代码到github。 一. 注册Git账户以及创建仓库 要想使用github第一步当然是注册github账号了。之后就可以创建仓库了&#xff08;免费用户只能建公共仓库&#xff0…

C#如何打包EXE程序生成setup安装文件

C#如何打包EXE程序生成setup安装文件作为研发人员&#xff0c;在本机上开发的winform wpf或者控制台程序需要发给其他人测试时候&#xff0c;一般需要对其进行打包生成setup安装文件&#xff0c;今天第一次&#xff0c;搜了下资料&#xff0c;记录如下&#xff1a;注&#xff1…

最具戏剧性的分析诊断案例——十分钟锁定数据库性能“元凶”

昨天&#xff0c;正好有点空时间想看看书&#xff0c;结果&#xff0c;刚打开书&#xff0c;没看几个字儿&#xff0c;接到用户电话说&#xff1a;一个库有问题&#xff0c;希望能帮忙看下。因为我知道他们那边也有自己的专职DBA&#xff0c;于是问&#xff1a;没让人给看看吗&…

Python黑科技:在家远程遥控公司电脑,python+微信一键连接!

有时候需要远程家里的台式机使用&#xff0c;因为我平时都是用 MAC 多&#xff0c;但是远程唤醒只能针对局域网&#xff0c;比较麻烦&#xff0c;于是我想用微信实现远程唤醒机器。 *注意&#xff1a;全文代码可左右滑动查看 准备工作 本程序主要是实现远程管理 Windows10操作系…

c#通过app.manifest使程序以管理员身份运行

通常我们使用c#编写的程序不会弹出这个提示&#xff0c;也就无法以管理员身分运行。微软的操作系统使用微软的产品方法当然是有的&#xff0c;通过app.manifest配置可以使程序打开的时候&#xff0c;弹出UAC提示需要得到允许才可以继续&#xff0c;这样就获得了管理员的权限来执…

MOS管基本认识(快速入门)

1. 三个极的判定G极(gate)—栅极&#xff0c;不用说比较好认 S极(source)—源极&#xff0c;不论是P沟道还是N沟道&#xff0c;两根线相交的就是 D极(drain)—漏极&#xff0c;不论是P沟道还是N沟道&#xff0c;是单独引线的那边2. N沟道与P沟道判别箭头指向G极的是N沟道 箭头背…

基础构建模块

5 基础构建模块 Java平台类库包含了丰富的并发基础构建模块&#xff0c;例如线程安全的容器类以及各种用于协调多个相互协作的线程控制流的同步工具类(Synchronizer)。本章将介绍其中一些最有用的并发构建模块。 5.1同步容器类 同步容器类包括Vector和Hashtable&#xff0c;二者…

TCP定时器

1. TCP中7种定时器 TCP中有7中定时器 &#xff08;1&#xff09;建立连接定时器(connection-establishment timer) &#xff08;2&#xff09;重传定时器(retransmission timer) &#xff08;3&#xff09;延迟应答定时器(delayed ACK timer) &#xff08;4&#xff09;坚持定时…

软件工程网络15个人阅读作业1 201521123038 游舒婷

软件工程网络15个人阅读作业1 201521123038 游舒婷 1.博客园地址 sakurai3104 2.码云地址 sakurai3104 3.阅读与思考 &#xff08;1&#xff09;回想一下你初入大学时对网络工程专业的畅想 当初你是如何做出选择网络工程专业的决定的&#xff1f; 填报志愿的时候&#xff0c;大…

18LaTeX学习系列之---LaTeX的参考文献

目录 目录前言&#xff08;一&#xff09;简单的参考文献1.说明2.源代码3.输出效果&#xff08;二&#xff09;以文件管理的方式1.说明&#xff1a;2.源代码&#xff1a;3.输出效果&#xff08;三&#xff09;直接从源网站获取1.说明&#xff12;.操作目录 本系列是有关LaTeX的…

Python 基础(常用数据结构)

常用数据结构 1&#xff09;元组 元组是一种静态的数据结构&#xff0c;无法修改&#xff0c;若要修改只能重新生成新的元组。 输出结果&#xff1a; 元组元素的获取是通过索引值去获得的&#xff1b;例如上面的tup1[0]返回apple&#xff1b;另外你可以直接把tup1一次性赋给多个…

Java IO(二)——RandomAccessFile

一、RandomAccessFile RandomAccessFile类可以说是Java语言中功能最为丰富的文件访问类&#xff0c;它提供了众多的文件访问方法。RandomAccessFile类支持"随机访问"方式&#xff0c;可以跳转到文件的任意位置处读写数据。要访问一个文件的时候&#xff0c;不想把文件…

centos7 安装python3

1.查看是否已经安装Python CentOS 7.2 默认安装了python2.7.5 因为一些命令要用它比如yum 它使用的是python2.7.5。 使用 python -V 命令查看一下是否安装Python 然后使用命令 which python 查看一下Python可执行文件的位置 可见执行文件在/usr/bin/ 目录下&#xff0c;切换到该…

centos svn 的搭建

一. SVN 简介 Subversion(SVN) 是一个开源的版本控制系統, 也就是说 Subversion 管理着随时间改变的数据。 这些数据放置在一个中央资料档案库(repository) 中。 这个档案库很像一个普通的文件服务器, 不过它会记住每一次文件的变动。 这样你就可以把档案恢复到旧的版本, 或是浏…

软件建模——第3章 项目前期

3.1 项目前期的主要工作 3.1.1 现状分析 1.硬件分析 2.软件分析 3.1.2 需求收集 3.1.3 粗略设计 1.体系结构设计 2.硬件&#xff08;网络&#xff09;设计 3.应用系统设计 4.安全设计 5.配套设计 3.1.4 可行性分析 3.2 结构化的项目前期实例 3.2.1 组织分析&#xff08;自动化…

echarts_部分图表配置_图表click事件

额。。当然其他事件都是支持的&#xff0c;此文仅以click做简单介绍&#xff1a; 请点击“柱子”。。。 官方介绍&#xff1a;http://echarts.baidu.com/tutorial.html#ECharts%20%E4%B8%AD%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%92%8C%E8%A1%8C%E4%B8%BA 1 function bottom_z (thisI…

mysql 5.7.25 的安装与 安装错误修改 适用于5.7解压版

1&#xff0c;根据自己的需求去官网下载 2.解压到自己喜欢的路径 其中date 和 my.ini 在5.7后面的版本 解压后是没有的&#xff0c;需要自己配置。 可以自己创建my.ini文件 &#xff0c;但是不能自己创建date文件夹。 其中my.ini文件的配置如下&#xff08;如果报错要将目录文件…