`

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 并发编程实战.pdf

    在《Java并发编程实战》中,读者也能够了解到如何将并发与现代Java语言特性结合起来,例如使用Lambda表达式和Stream API来简化并发代码的编写。 综合来看,该书不仅适合于对Java并发编程感兴趣的初学者,同样也适合...

    java并发编程实战源码,java并发编程实战pdf,Java

    《Java并发编程实战》是Java并发编程领域的一本经典著作,它深入浅出地介绍了如何在Java平台上进行高效的多线程编程。这本书的源码提供了丰富的示例,可以帮助读者更好地理解书中的理论知识并将其应用到实际项目中。...

    《java 并发编程实战高清PDF版》

    《Java并发编程实战》是一本深入探讨Java平台并发编程的权威指南。这本书旨在帮助开发者理解和掌握在Java环境中创建高效、可扩展且可靠的多线程应用程序的关键技术和实践。它涵盖了从基本概念到高级主题的广泛内容,...

    java并发编程2

    Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。以下是对标题和描述中所提及的几个知识点的详细解释: 1. **线程与并发** - **线程*...

    Java并发编程实践高清pdf及源码

    通过学习《Java并发编程实践》,开发者将能够更好地理解和利用Java平台的并发特性,编写出更高效、更可靠的多线程应用程序。无论是初级开发者还是经验丰富的专业人员,都能从这本书中收获宝贵的并发编程知识。

    java 并发编程的艺术pdf清晰完整版 源码

    通过阅读《Java并发编程的艺术》这本书,开发者不仅可以掌握Java并发编程的基础知识,还能了解到一些高级特性和技巧,从而在实际开发中游刃有余。同时,附带的源码将有助于加深理解,提供实际操作的机会。

    java并发编程内部分享PPT

    总的来说,这份“java并发编程内部分享PPT”涵盖了Java并发编程的多个重要方面,包括线程创建与管理、同步机制、并发容器、线程池、并发问题以及异步计算。通过深入学习和实践这些知识点,开发者可以更好地应对多...

    JAVA并发编程实践.pdf+高清版+目录 书籍源码

    《JAVA并发编程实践》这本书是Java开发者深入理解并发编程的重要参考资料。它涵盖了Java并发的核心概念、工具和最佳实践,旨在帮助读者在多线程环境下编写高效、安全的代码。 并发编程是现代软件开发中的关键技能,...

    java并发编程书籍

    Java并发编程是软件开发中的一个关键领域,尤其是在大型企业级应用和分布式系统中。通过学习相关的书籍,开发者可以深入理解如何有效地设计和实现高效的多线程应用程序,避免并发问题,如竞态条件、死锁、活锁等。...

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

    ### Java并发编程:volatile关键字解析 #### 一、内存模型的相关概念 在深入了解`volatile`关键字之前,我们首先需要理解计算机内存模型的一些基本概念。在现代计算机系统中,CPU为了提高执行效率,会将频繁访问的...

    JAVA并发编程艺术pdf版

    《JAVA并发编程艺术》是Java开发者深入理解和掌握并发编程的一本重要著作,它涵盖了Java并发领域的核心概念和技术。这本书详细阐述了如何在多线程环境下有效地编写高效、可靠的代码,对于提升Java程序员的技能水平...

    java并发编程与实践

    "Java并发编程与实践"文档深入剖析了这一主题,旨在帮助开发者理解和掌握如何在Java环境中有效地实现并发。 并发是指在单个执行单元(如CPU)中同时执行两个或更多任务的能力。在Java中,这主要通过线程来实现,...

    Java并发编程从入门到精通(pdf)(附源码)

    本书首先会介绍Java并发编程的基础概念,包括线程的创建与管理、同步机制如synchronized关键字和Lock接口,以及如何避免常见的并发问题,如死锁、活锁和饥饿。这些基础知识是理解并发编程的基石,通过深入浅出的讲解...

    Java并发编程设计原则和模式

    本资料“Java并发编程设计原则和模式”深入探讨了如何在Java环境中有效地进行并发处理,以充分利用系统资源并避免潜在的并发问题。 一、并发编程基础 并发是指两个或多个操作在同一时间段内执行,但并不意味着这些...

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

    总结来说,`volatile`关键字在Java并发编程中起到了关键的作用,但其功能有限,不能替代其他的同步机制。开发者应根据具体场景选择合适的并发工具,以确保数据的正确性和程序的稳定性。在处理复杂的数据结构或涉及...

    Java并发编程书籍高清版

    本资源包含三本权威的Java并发编程书籍:《Java并发编程实践》、《java并发编程的艺术》以及Brian Goetz的文字版《Java并发编程实践》。 首先,我们来看《Java并发编程实践》(Java Concurrency in Practice)这...

    Java并发编程设计原则与模式.pdf

    《Java并发编程设计原则与模式》是Java并发编程领域的一部经典著作,作者Doug Lea在并发编程领域有着深厚的造诣。这本书详细介绍了如何在Java环境中有效地设计和实现并发程序,涵盖了众多关键概念、设计原则和实用...

    java并发编程

    Java并发编程是Java开发者必须掌握的关键技能之一,它涉及到如何在多线程环境中高效、安全地执行程序。并发编程能够充分利用多核处理器的计算能力,提高应用程序的响应速度和整体性能。《Java编程并发实战》这本书是...

    Java并发编程实践.pdf

    ### Java并发编程实践 #### 一、并发编程基础 ##### 1.1 并发与并行的区别 在Java并发编程中,首先需要理解“并发”(Concurrency)和“并行”(Parallelism)的区别。“并发”指的是多个任务在同一时间段内交替...

Global site tag (gtag.js) - Google Analytics