synchronized 原理分析

synchronized 原理分析

1. synchronized 介绍

   在并发程序中,这个关键字多是出现频率最高的一个字段,他能够避免多线程中的安全问题,对代码进行同步。同步的方式其实就是隐式的加锁,加锁过程是有 jvm 帮咱们完成的,再生成的字节码中会有体现,若是反编译带有不可消除的 synchronized 关键字的代码块的 class 文件咱们会发现有两个特殊的指令 monitorentermonitorexit ,这两个就是进入管程和退出管程。为何说不可消除的 synchronized ,这是因为在编译时期会进行锁优化,好比说在 StringBuffer 中是加了锁的,也就是锁对象就是他本身,然而咱们编译之后会发现根本没有上面的两条指令就是由于,锁消除技术。java

   Synchronized 使用的通常场景,在对象方法和类方法上使用,以及自定义同步代码块。可是在方法上使用 Synchronized 关键字和使用同步代码块是不同的,方法上采用同步是采用的字节码中的标志位 ACC_SYNCHRONIZED 来进行同步的。而同步代码块则是采用了对象头中的锁指针指向一个监视器(锁),来完成同步。数组

   当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先获取 monitor ,获取成功以后才能执行方法体,方法执行完后再释放 monitor 。在方法执行期间,其余任何线程都没法再得到同一个 monitor 对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需经过字节码来完成。安全

2. 对象头和锁

   一个对象在内存中分为三部分:对象头、实例数据、对齐填充。数据结构

  1. 对象头中主要存放了 GC 分代年龄、偏向锁、偏向 id、锁类型、hash 值等。jvm 通常会用两个字来存放对象头,(若是对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成。MarkWord里默认数据是存储对象的HashCode等信息,可是会随着对象的运行改变而发生变化,不一样的锁状态对应着不一样的记录存储方式
  2. 实例数据就包括对象字段的值,不只有本身的值还有继承自父类的字段的值。通常字段的顺序是同类型的字段放在一块儿,空间比较大的字段放在前面。在知足上面的规则下父类的放在子类的前面。多线程

  3. 对其填充并不是必要的,整个对象须要是 8 字节的整数倍,当不足的时候会进行填充以达到 8 字节整数倍,主要仍是为了方便存取。并发

   这里咱们主要分析一下重量级锁也就是一般说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(在 Synchronized 代码块中的监视器 )的起始地址。每一个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构以下。jvm

ObjectMonitor() {
    _count        = 0; //记录个数
    _owner        = NULL; // 运行的线程
    //两个队列
    _WaitSet      = NULL; //调用 wait 方法会被加入到_WaitSet
   _EntryList    = NULL ; //锁竞争失败,会被加入到该列表
  }

   ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。
性能

3. Synchronized 代码块原理

反编译下面的代码获得的字节码以下:优化

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("hello");
        }
    }

    public synchronized void test(){

    }
}

   当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程能够成功取得 monitor,并将计数器值设置为 1,取锁成功。若是当前线程已经拥有 objectref 的 monitor 的持有权,那它能够重入这个 monitor ,重入时计数器的值也会加 1。假若其余线程已经拥有 objectref 的 monitor 的全部权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其余线程将有机会持有 monitor 。值得注意的是编译器将会确保不管方法经过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而不管这个方法是正常结束仍是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然能够正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理全部的异常,它的目的就是用来执行 monitorexit 指令。因此看到上面有两条 monitorexit !线程

4. Synchronized 方法原理

   先看一个反编译的实例方法的结果,确实比普通的方法多了一个标志字段。方法级的同步是隐式,即无需经过字节码指令来控制的,它实如今方法调用和返回操做之中。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有 monitor , 而后再执行方法,最后再方法完成(不管是正常完成仍是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其余任何线程都没法再得到同一个monitor。若是一个同步方法执行期间抛 出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放。

5. 偏向锁

   偏向锁是 Java 为了提升程序的性能而设计的一个比较优雅的加锁方式。偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再作获取锁的过程。若是有其余线程竞争锁的时候就须要膨胀为轻量级锁。这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。

   因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁,不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。

   偏向锁获取的过程以下,当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操做把获取到这个锁的线程的ID记录在对象的Mark Word之中的偏向线程ID,并将是否偏向锁的状态位置置为1。若是CAS操做成功,持有偏向锁的线程之后每次进入这个锁相关的同步块时,直接检查ThreadId是否和自身线程Id一致,
若是一致,则认为当前线程已经获取了锁,虚拟机就能够再也不进行任何同步操做(例如Locking、Unlocking及对Mark Word的Update等)。

   其实通常来讲偏向锁不多又说去主动释放的,由于只有在其余线程须要获取锁的时候,也就是这个锁不只仅被一个线程使用,可能有两个线程交替使用,根据对象是否被锁定来决定释放锁(恢复到未锁定状态)仍是升级到轻量锁状态。

6.轻量级锁

   轻量级锁,通常指的是在有两个线程在交替使用锁的时候因为没有同时抢锁属于一种比较和谐的状态,就可使用轻量级锁。他的基本思想是,当线程要获取锁时把锁对象的 Mark Word 复制一份到当前线程的栈顶,而后执行一个 CAS 操做把锁对象的 Mark Word 更新为指向栈顶的副本的指针,若是成功则当前线程拥有了锁。能够进行同步代码块的执行,而失败则有两种可能,要么是当前线程已经拥有了锁对象的指针,这时能够继续执行。要么是被其余线程抢占了锁对象,这时候说明了在同一时间有两个线程同时须要竞争锁,那么就打破了这种和谐的局面须要膨胀到重量级锁,锁对象的标志修改,获取线程的锁等待。    在轻量级锁释放的过程就采用 CAS 把栈上的赋值的 Mark Word 替换到锁对象上,若是失败说明有其余线程执抢占过锁,锁对象的 Mark Word 的标志被修改过,在释放的同时唤醒等待的线程。

相关文章
相关标签/搜索