`

java并发编程三特性与volatile

阅读更多

前言

 

前面讲过使用synchronized关键字来解决“线程安全”问题,其本质是将“并行”执行改“串行”,也就是所谓的“同步”,前面也讲过这种方式的代价较高。在java中还提供一种弱化版的同步机制:volatile变量。

 

为什么说是弱化版的同步机制呢?首先看下在使用synchronized关键字保证的 (强)同步机制的三个特性说起:原子性、可见性、有序性,也就是说使用synchronized加锁可以同时保证程序执行过程中的原子性、可见性、有序性。

 

1、原子性:

 

这个特性更事务处理中的原生性有点类似:单个或多个操作是作为整体一起执行,要么全部执行,要么都不执行。但也有区别:事务里强调的是回滚,而并发编程中强调的是“作为不可拆分的整体执行”。这里提到“单个操作”和“多个操作”。

 

操作系统中的“单个操作”是原子性的,在java中“单个操作”是原子性操作的有:

longdouble之外的基本类型的赋值操作,比如int i=1

所有引用类型的赋值操作,比如Object obj=xx

原子API java.concurrent.Atomic.* 包中的类对应的操作,比如AtomicInteger 的自增操作getAndIncrement

 

这里需要注意的是longdouble的赋值有可能不是原子性的,它们在java中占8个字节,一个字节8bit,一共就是64bit。在32位的操作系统中,每次原子赋值只能对32bit进行操作,也就是说在32位的操作系统中对longdouble的赋值其实是两个操作。“多个操作”的原子性,只能通过加锁方式来保证。

 

“多个操作”的原子性,前面已经提到了可以通过synchronized关键字或者Lock(新锁API)加锁来实现。通过串行的方式,保证每次只有一个线程在执行“多个操作”,让同步代码块或同步方法看起来是一个不可分割的整体。

 

需要注意的是 i++i--++i--i等都不是原子性操作,i++可以拆分为i+1操作和对i重新复制操作。

 

另外通过new创建对象也不是原子操作,一共有三个操作:分配内存空间;初始化对象;指向该对象的内存地址。

 

2、可见性:

 

这是一个相对来说比较难以理解的概念,其它类似文章中的说法是“变量值”在工作内存与主存之间的同步不一致,会导致可见性问题。在这里换一种说法,可能会帮助大家更好的理解。还记得么(详见这里)java的内存结构分为: 方法区、堆区、vm栈、本地方法栈、程序计数器。这里要说的重点是vm 、方法区、堆区,所谓“工作内存”其实就是每个线程对应的“vm栈”内存,所谓“主存”可以理解为方法区和堆区。线程、vm栈、方法区、堆区 它们之间的关系如下:



 

线程1在执行某个方法时,会创建一个vm栈,该方法中使用了一个“方法区”中的静态变量,此时会读取一份方法区中变量值作为副本 放入vm栈内存中。假设现在有另外一个线程2改变了方法区中该静态变量值,在线程1vm栈中其实存放的还是“旧值”,示意图如下:



 

           (这里只是以静态变量为例,如果是对象的成员变量主存就是堆区)

 

可以看到线程1i的值始终是0,线程2中的值是1(主存中的值也变为1),这就出现两个线程中读取同一个变量时,出现不一致现象,这就是java并发编程中的“可见性”问题。

 

java中解决可见性问题的方案,有两种:第一种就是前面提到的“加锁”,把并行操作变量i的值 改为“串行”,由于同一时刻只有一个线程在操作主存,所以不存在两个线程看到的值不一致的问题;第二种办法就是对i变量采用volatile关键字修饰,如下:

public volatile static int i=0;

与加锁方式不同的是,volatile关键字只保证“可见性”,而加锁的方式可以同时保证:原子性、可见性、有序性,所以是volatile关键字“弱化版”的同步机制。并且复出的性能代价也比加锁方式小很多,因为此时多线程可以照常“并行”执行。

 

volatile的核心思想就是,告诉各个线程在读取这个变量时,每次都从主存中读取,从而保证线程中每次获取到的都是最新值,以解决“可见性”问题;而不是只读一次放入vm栈副本中,以后使用时都直接读取副本。对线程执行来说,从vm栈中获取数据的性能肯定比每次都从主存读取性能要好,所以使用volatile关键字也有些许性能损失,但仍能保证多线程并行执行,相对加锁方式来说 性能会有大幅度提高。使用volatile修饰后,i变量在多个线程中的可见性示意图如下:

 




 

可以看到,在同一时刻多个线程中看到的i值是相同。但不是所有的情况都可以使用volatile关键字,由于volatile关键字只能保证“可见性”,事实上它只适用少有的几种情况。关于volatile关键字的适用场景放到最后讲。接着看第三个并发问题“有序性”:

 

3、有序性:

 

所谓有序性就是代码的执行顺序是从前往后依次执行。我们期望的代码执行顺序是我们编码的顺序,比如在同一个方法中有下列代码:

 

int i=0;//语句1
int j=0; //语句2
i=i+1; //语句3
j=j+1; //语句4

 

 

我们期望的执行顺序是:语句1、语句2、语句3、语句4顺序执行,但在jvm的真实实现中有可能是:语句1、语句3、语句2、语句4。问什么呢jvm要这样实现呢?这又回到“vm栈”的入栈和出栈问题,我们都知道“栈”的数据结构是“先进先出”。

 

如果按照:语句1、语句2、语句3、语句4顺序执行,首先是变量i入栈-->然后变量i出栈-->变量j入栈-->变量j出栈-->变量i再入栈并执行+1操作-->变量i再出栈-->变量j再入栈并执行+1操作-->变量j出栈。

 

如果按照:语句1、语句3、语句2、语句4执行,首先变量i入栈-->执行+1操作 出栈-->变量j入栈-->执行+1操作 出栈。可以看到如果采用这种方式,会减少入栈出栈的操作次数,这就是jvm在不影响执行结果的前提下(这里指的单线程),为了优化变量的入栈和出栈,对执行的代码重新排序,也就是所谓的“指令重排”。指令重排的依据是:执行效率最优;执行有依赖关系的必须提前执行,满足这两个条件即可。比如前面语句中必须要先执行语句1,才能执行语句3

 

需要注意的是有个限定“不影响执行结果的前提”,这里指的是单线程,在多线程并发执行的情况下可能出现意想不到的结果,比如:

 

public class Main1 {
    boolean flag=false;
    Source source = null;
 
    public void getConnect(){
        source=getSource();//语句A
        flag=true;//语句B
    }
 
    public void doSelect(){
        if(flag == true){
            source.getMsg();
        }
    }
}

 

 

语句AB由于没有依赖,可能发生指令重排。

但在单线程下先执行getConnect()方法,再执行doSelect(),程序没有任何问题。

在多线程环境下就不同了,假设线程1执行getConnect()方法;同时线程2执行doSelect()方法,由于语句AB执行重排,这时可能出现空指针(当然这里也可能是由于“可见性”导致)。

 

volatile关键字可以一定程度上消除指令重排 即:在volatile变量之前和之后的指令会被分割开,比如下列语句:

 

int i=0;//语句1
int j=0; //语句2
flag=ture;//flag是volatile变量
i=i+1; //语句3
j=j+1; //语句4

 

 

上述语句只可能出现语句12重排,语句34重排。相当于在volatile变量处建立了一道屏障,这就是所谓的“内存屏障”。

 

并发编程中的“有序性”问题,指的就是在多线程环境下由于指令重排导致的程序执行的不一致问题(即 线程安全问题)。解决有序性问题,有两种办法:

1、使用synchronizedLock加锁:前面说过,指令重排在单线程中不会影响执行结果,通过加锁并行改串行,串行本质上就是单线程执行的变体。

2、在某些场景下可以使用volatile变量,使用volatile变量可以一定程度上消除“指令重排”,一定程度上保证“有序性”。

注意两者的区别,加锁本质上没有消除“指令重排”。

 

再聊volatile

 

相对于加锁来说volatilejava中轻量版的“同步机制”,主要表现在volatile无法保证多个操作的“原子性”,只能保证“可见性”和防止“指令重排”。典型错误使用volatile场景一:

public class Main1 {
    volatile int num = 0;
 
    public void plus(){
        num++;//非原子操作 多线程环境下存在线程安全问题
    }
 
    public void doSelect(){
        num--;//非原子操作 多线程环境下存在线程安全问题
    }
}
 

 

也就是说如果要使用volatile保证线程安全,那volatile修饰的变量必须只进行原子性操作,即修饰的变量只能进行如下操作:

longdouble之外的基本类型的赋值操作,比如int i=1

所有引用类型的赋值操作,比如Object obj=xx

原子API java.concurrent.Atomic.* 包中的类对应的操作,比如AtomicInteger 的自增操作getAndIncrement

 

另一错误使用volatile场景,就是错误的认为new Object()是原子性操作。还记得双重检查单例模式的实现么,如果new Object()是原子操作的话,多线程下的单例模式是这样:

public class Singleton2 { 
  
    //注意必须是volatile修饰,保证多线程下数据的可见性 
    private volatile static Singleton2 singleton2 = null; 
  
    private Singleton2(){ 
  
    } 
  
    public static Singleton2 getInstance(){ 
        if(singleton2 == null){//第一重检查 
             ingleton2 = new Singleton2(); 
        } 
        return singleton2; 
    } 
} 

 

 

这是错误的实现方式,由于new Singleton2()其实包含三个操作,多个操作要保证原子性,只能通过加锁实现,正确的实现方式详见这里,不再累述。

 

 

所以volatile相对加锁来说性能虽好,但真实的运用场景却很少,典型场景有两种:第一种就是做开关标记;第二种就是配合加锁实现“双重检查加锁单例模式”。

  • 大小: 42.6 KB
  • 大小: 48.1 KB
  • 大小: 40.9 KB
  • 大小: 38.2 KB
0
0
分享到:
评论

相关推荐

    Java并发编程系列- volatile

    Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;

    Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析

    Java并发编程(5)volatile变量修饰符-意料之外

    Java并发编程(5)volatile变量修饰符—意料之外的问题(含代码)编程开发技术共6页.pdf.zip

    Java并发编程实战

    第三部分 活跃性、性能与测试 第10章 避免活跃性危险 第11章 性能与可伸缩性 第12章 并发程序的测试 第四部分 高级主题 第13章 显式锁 第14章 构建自定义的同步工具 第15章 原子变量与非阻塞同步机制 第16章...

    Java 并发核心编程

    本文的主题是关于具有java语言风格的Thread、synchronized、volatile,以及J2SE5中新增的概念,如锁(Lock)、原子性(Atomics)、并发集合类、线程协作摘要、Executors。开发者通过这些基础的接口可以构建高并发、线程...

    java并发编程与内存模型

    描述java并发编程原理 一.内存模型的相关概念 二.并发编程中的三个概念 三.Java内存模型 四..深入剖析volatile关键字 五.使用volatile关键字的场景

    Java并发编程的艺术.md

    《Java并发编程的艺术》笔记 第一章 并发编程的挑战 第二章 Java并发机制的底层实现原理 volatile的两条实现原则: 1. Lock前缀指令会引起处理器缓存回写到内存 2. 一个处理器的缓存回写到内存会导致其他...

    Java并发编程:设计原则与模式(第二版)

    java并发方面的两大名著之一。读者将通过使用java.lang.thread类、synchronized和volatile关键字,以及wait、notify和notifyall方法,学习如何初始化、控制和协调并发操作。此外,本书还提供了有关并发编程的全方位...

    java并发编程

    本书全面介绍了如何使用Java 2平台进行并发编程,较上一版新增和扩展的内容包括:, ·存储模型 ·取消 ·可移植的并行编程 ·实现并发控制的工具类, Java平台提供了一套广泛而功能强大的api,工具和技术。...

    Java 并发编程实战

    第三部分 活跃性、性能与测试 第10章 避免活跃性危险 第11章 性能与可伸缩性 第12章 并发程序的测试 第四部分 高级主题 第13章 显式锁 第14章 构建自定义的同步工具 第15章 原子变量与非阻塞同步机制 第16章...

    Java并发编程原理与实战

    深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 AbstractQueuedSynchronizer(AQS)详解.mp4 使用AQS重写自己的锁.mp4 重入锁原理与演示....

    Java并发编程学习笔记

    2、可重入锁与 Synchronized 的其他特性 3、ThreadLocal 的底层实现与使用 4、ReentrantLock底层实现和如何使用 5、Condition源码分析 6、ReentrantReadWriteLock底层实现原理 7、并发工具类CountDownLatch 、...

    龙果 java并发编程原理实战

    龙果 java并发编程原理实战 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四...

    Java 并发编程原理与实战视频

    java并发编程原理实战 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个...

    java并发编程面试题

    java并发编程 基础知识,守护线程与线程, 并行和并发有什么区别? 什么是上下文切换? 线程和进程区别 什么是线程和进程? 创建线程有哪几种方式?,如何避免线程死锁 线程的 run()和 start()有什么区别? 什么是 ...

    java并发编程理论基础精讲

    本资源为您提供了关于 Java 并发编程理论基础的精讲,涵盖了多线程编程的核心概念、基本原理以及在 Java 中的应用。通过深入学习,您将建立坚实的并发编程基础,能够更好地理解和应对多线程编程中的挑战。 并发编程...

    汪文君高并发编程实战视频资源下载.txt

    │ Java并发编程.png │ ppt+源码.rar │ 高并发编程第二阶段01讲、课程大纲及主要内容介绍.wmv │ 高并发编程第二阶段02讲、介绍四种Singleton方式的优缺点在多线程情况下.wmv │ 高并发编程第二阶段03讲、...

    Java并发编程(18)第五篇中volatile意外问题的

    Java并发编程(18)第五篇中volatile意外问题的正确分析解答(含代码)编程开发技术共5页.pdf.zip

Global site tag (gtag.js) - Google Analytics