java 锁

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

  原子性:一个或多个操做要么所有执行成功要么所有执行失败;java

   可见性:当多个线程访问同一个变量时,若是其中一个线程对其做了修改,其余线程能当即获取到最新的值;编程

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

 

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

  CPU愈来愈快,主存逐渐跟不上cpu的频率,cpu须要等待主存浪费资源。因此cache的出现主要为了解决cpu和内存之间频率不匹配的问题。多线程

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

  

3、重排序

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

  重排序分类

    

    一、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。jvm

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

      若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。优化

    三、内存系统的重排序:因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。以下图:spa

    

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

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

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

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

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

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

 
    
 

  JMM禁止重排序的措施:

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

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

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

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

    一、在每一个volatile写操做前插入StroreStore屏障 
    二、在每一个volatile写操做前插入StroreLoad屏障 
    三、在每一个volatile读操做前插入LoadLoad屏障 
    四、在每一个volatile读操做前插入LoadStore屏障

 

4、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的内存语义

    一、第二个操做是volatile写时,第一个操做不论是什么,都不能重排序 
    二、第一个操做是volatile读时,第二个操做不论是什么,都不能重排序 
    三、第一个操做是volatile是写,第二个操做是volatile是读,不能重排序

  volatile和synchronized的区别: 

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

    synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住。

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

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

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

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

 

5、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 的缺点

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

    二、Java1.6为 synchronized 作了优化,增长了从偏向锁到轻量级锁再到重量级锁的过分,可是在最终转变为重量级锁以后,性能仍然较低。

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

    

 

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

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

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

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

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

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

  

相关文章
相关标签/搜索