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发送消息。

  

    

  

原文地址:https://www.cnblogs.com/jiangyaxiong1990/p/10358043.html