zoukankan      html  css  js  c++  java
  • 并发编程学习(2)----volatile与synchronized

      此次文章主要探讨volatile与synchronized,通过一些基础概念的介绍,让读者对于两者有更深的了解。

    一、几个相关概念

    1、原子性

      其本意是“不能被进一步分隔的最小粒子”,而原子操作意为“不可被中断的一个或一系列操作”。在多处理器重实现原子操作变得有点复杂。

    1)操作系统如何实现原子性。

      单处理器可以对同一个缓存行里自动进行16/32/64位的原子操作。但是复杂的内存操作处理器是不能保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。例如,i++是一个读改写的操作,由于该代码可能被不同的线程执行导致最终出现的结果可能不是我们想要的结果(具体原因不在此赘述)。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

    a。使用总线锁保证原子性

      处理器通过使用总线锁来解决i++问题。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,此时该处理器可以独占共享内存。

    b。使用缓存锁来保证原子性

      总线锁把CPU与内存间的通信锁住了,这使得在锁定期间,其它处理器不能操作其它内存地址的数据,所以总线锁开销比较大。我们只需要保证对某个内存地址的操作是原子性即可(减小锁粒度)。目前处理器在某些场合下回使用缓存锁定来代替总线锁来进行优化。

    2、可见性

      可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。

    3、指令重排

      重排序指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。在多线程的程序中,对某些指令的重排序可能会改变程序的执行结果(后续会有实例说明)。

    二、volatile

    1、volatile变量具有以下特性。

      可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入、

      禁止重排序:jdk1.5以后对volatile语义进行了加强,不允许volatile变量之间进行重排序。

    2、底层实现原理

    1)操作系统层面

      操作系统可通过LOCK#前缀指令实现以上前两个特性。为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1、L2或其它)后再进行操作,但操作完不知道何时写回到内存(操作系统这里其实使用了异步操作来解决生产消费速度不均的问题)。如果申明了volatile的变量进行写操作,JVM就会想处理器发送一条Lock前缀指令,将这个变量的缓存行的数据写回到内存中。此时,其它处理器中的值还是旧值。在多处理器下,为了保证各个处理器缓存一致,实现了缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行过期时,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改时,会重新从操作系统内存中把数据读取到处理器缓存中。

      a。Lock前缀指令会引起处理器缓存回写到内存。

      Lock前缀指令导致在执行指令期间,声言处理器的Lock信号。在多处理器环境下,处理器可以独占任何共享内存。操作系统通过总线锁或者缓存锁定,来确保同时只能有一个处理器可修改缓存数据。

      b。一个处理器的缓存会写到内存导致其它处理器的缓存无效。

    2)JMM层面。

      在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数和异常处理参数不会在线程之间共享,它们不会有内存可见性问题。

      从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了改线程共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。JMM的抽象示意图如下所示。

      当一个变量被申明为volatile时。

      写入操作:JMM会把该线程对应的本地内存中的变量刷新到主存中。

      读取操作:JMM会把本地内存置为无效,线程接下来会从主存中读取共享变量。

            

    3)禁止重排序应用

      在单例模式中,人们使用了双重校验来降低锁同步的开销,查看以下无volatile时的代码。

      

      以上是一个错误的优化,当线程执行到第4行时,代码读取到instance不为null,但是注意此时instance引用的对象还未初始化。原因如下。

      instance = new Singleton()可以分解为如下3行伪代码。

        memory = allocate();//1.分配对象的内存空间。

        ctorInstance(memory);//2.初始化对象

        instance = memory;// 3.设置instance指向刚刚分配的地址

      步骤2和3由于指令重排,可能导致另一个线程访问到未被初始化的对象。如果在instance变量前加上volatile即可解决此问题。

     三、synchronized

    1、简单介绍

      synchronized简单的理解就是对象锁,Java中的每一个对象都可以作为锁。它主要可以确保代码一系列操作在同一线程只能由一个线程访问。

    具体表现为一下3种形式。

      对于普通同步方法,锁是当前实例对象。

      对于静态同步方法,锁是当前的Class对象。

      对于同步方法块,锁是synchronized括号里配置的对象。

    2、原理介绍。

      在Java中任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入到同步块或者同步方法中,而没有获取到监视器的线程将会被阻塞在同步块或者同步方法入口处,进入BLOCKED状态。当访问访问Object的前驱(获得了锁的线程)释放了锁,则会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器获取。具体过程如下图。

             

    四、synchronized和volatile比较

    1、原理分析

      从原理上分析,volatile是JMM借助操作系统底层指令实现的关键字。当变量被它修饰时,它可以保证对于该变量的访问都需要从共享内存中获取,而每次修改则必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。synchronized是JVM层面的,它借助Java对象的monitor实现的,同一时刻只能有一个线程进入监视器,它可以保证程序对于变量访问的可见性和排它性。

      为了实现线程间的同步,两者都是通过总线锁/内存锁定、monitor这样的方式,即线程在同一时刻只能访问对应的内存区域,这样就避免了多个线程同时写入内存导致结果无法预知的情况。

    2、特性对比

    1)volatile

      a.可见性:可保证变量在内存中的可以性。

      b.多数情况下相对synchronized具有较高的性能

      c.有序性:在某些情况下可以防止指令重排(经典案例为单例双重校验)

    2)synchronized

      a.性能相对较差,但是1.6以后性能有所提升。

      b.有序性,被加锁的代码块同一时刻只能有一个线程访问程序,保证程序有序执行

    五、总结思考

    1)为了解决多线程间的同步问题核心思想:通过限定同一时刻仅有一个线程有写入。

    2)操作系统通过减小锁的粒度提升性能。

  • 相关阅读:
    docker清理无用资源
    为什么不需要在 Docker 容器中运行 sshd
    转载:SQL注入演示demo
    docker方式安装prometheus主控
    promethus监控结构
    oracle的函数
    oracle冷备份后恢复
    oracle的冷备份
    oracle常用
    oracle的系统文件的查询
  • 原文地址:https://www.cnblogs.com/han02216/p/8510501.html
Copyright © 2011-2022 走看看