zoukankan      html  css  js  c++  java
  • java 锁

    一、 Java并发编程的三个概念

      原子性:一个或多个操作要么全部执行成功要么全部执行失败;

       可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值;

       有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序);

    二、单核CPU到多核CPU的变化 

      CPU越来越快,主存逐渐跟不上cpu的频率,cpu需要等待主存浪费资源。所以cache的出现主要为了解决cpu和内存之间频率不匹配的问题。

    但cache也带来了新的问题,并发处理的不同步(不过可以通过总线锁和缓存一致性来解决)。

      

    三、重排序

      在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。

      重排序分类

        

        1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

        2、指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。

          如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

        3、内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。如下图:

        

      重排序的原则:as-if-serial语义

        as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。

        编译器、runtime和处理器都必须遵守as-if-serial语义。

        为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。数据依赖关系如下图所示:

        
         as-if-serial语义只能保证单线程下,重排序引起的问题。在多线程情况下,不存在数据依赖关系的重排序也会破坏程序的意图。
        

        单线程情况下,控制依赖关系的重排序,不影响最终结果。多线程情况下,则可能会破坏程序的意图。

     
        
     

      JMM禁止重排序的措施:

        1、对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。

        2、对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为MemoryFence)指令,

           通过内存屏障指令来禁止特定类型的处理器重排序。

      JMM的内存屏障插入策略:Load:加载(读)、Store:保存(写),屏障名称就可以看出读写的先后顺序) 

        1、在每个volatile写操作前插入StroreStore屏障 
        2、在每个volatile写操作前插入StroreLoad屏障 
        3、在每个volatile读操作前插入LoadLoad屏障 
        4、在每个volatile读操作前插入LoadStore屏障

    四、Volatile 

        volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。volatile不会引起上下文的切换和调度(禁止指令重排),执行开销更小。  

      Volatile 的特性:

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

        原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

      Volatile 的原理:

        1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令(ACC_VOLATILE)

        2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;

          即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

        3. 它会强制将对缓存的修改操作立即写入主存

        4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效

      volatile写-读建立的happens-before关系:

        volatile的写-读与锁的释放-获取有相同的内存效果。

        这里A线程写一个volatile变量后,B线程读同一个volatile变量。

        A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

      

      Volatile 写-读的内存语义:

        当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

        当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

        注:关于volatile变量重排序,严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

         

        

        JMM重排序分为编译器重排序和处理器重排序,JMM会限制这两种类型的重排序类型来保证volatile的内存语义

        1、第二个操作是volatile写时,第一个操作不管是什么,都不能重排序 
        2、第一个操作是volatile读时,第二个操作不管是什么,都不能重排序 
        3、第一个操作是volatile是写,第二个操作是volatile是读,不能重排序

      volatile和synchronized的区别: 

        volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;

        synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

        volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

        volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

        volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

        volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

    五、Synchronized 锁

        synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,

      其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中。

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

        对于静态同步方法,锁是当前类Class对象;

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

      synchronized 的原理

        加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令(利用javap -v *.class字节码文件)

        加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,

           如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

        其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

      synchronized 的缺点

        1、会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。

        2、Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

      锁的获取和释放 建立的happens-before关系

        

      锁的释放和获取的内存语义

         JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

        锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

        线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

        线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

        线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

      

        

      

  • 相关阅读:
    关于postman使用上发现的一点问题
    关于异步的处理方法
    关于console.log() 打印得引用类型得数据得相关问题
    使用electron将单页面vue webapp 打包成 PC端应用
    当后台只接受字符串得时候,在传输复杂得数据得时候会发生得问题
    默认事件
    事件冒泡
    offsetWidth clientWidth scrollWidth 三者之间的区别和联系
    事件获取目标 currentTarget target srcElement三者之间的区别和联系
    不支持模块化规范的插件可以使用import 导入的原因
  • 原文地址:https://www.cnblogs.com/jiangyaxiong1990/p/10358043.html
Copyright © 2011-2022 走看看