经过synchronized来看Java锁机制

1、何为锁住一个对象?

  1. 咱们常常说当使用synchronized 修饰一个代码块时,编译后会在同步块的先后分别造成monitorenter和monitorexit这两个字节码指令,而当虚拟机执行monitorenter指令时,会去尝试获取对象的锁,若是这个对象没有被锁定或者当前线程已经持有了这个对象的锁(可重入性),就会把锁的计数器加一,而在执行monitorexit指令时锁的计数器就会减一;
  2. 此时就很奇怪,我本身定义的对象哪来的锁呢?实际上是由于在堆中存储的每一个对象虚拟机都会为其生成一个对象头,对象头通常分为两部分(若是是数组对象则会分为三部分),而对这个问题最重要的是第一部分(Mark Word),通常为32bit或64bit(由虚拟机的位数决定),当该对象处于未被锁定的状态时,MarkWord中有25bit用来存储对象的hashCode(这里猜想是为了便于找到对象?与HashMap相似,但没有深刻去了解),4bit用于存储对象的分代年龄(GC相关),2bit用于存储锁标志位,1bit默认为0;

而当对象被不一样种类的锁锁定时其状态会变为:(注意此时是复用原有的空间的,并经过栈保存原有的MarkWord信息以便以后解除锁定的时候复原)

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是: 无锁状态、偏向锁状态、轻量级锁状态(CAS操做实现)和重量级锁状态(synchronized),这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率。

  1. 总结,当程序须要锁定一个对象时,所谓锁住一个对象即为改变该对象的MarkWord状态,让其余访问该对象的线程了解到这个对象处于被锁定的状态。

2、当锁住一个对象的时候有多个线程在等该如何去抉择?

2.1 非公平性:

  1. synchronized是非公平锁,即它不会按照线程等待的时间或其余因素排除优先级,而是当持有锁的对象释放锁后由一个随机的对象去得到锁;

图片来源参考文章: blog.csdn.net/javazejian/…

每一个实例对象都会拥有一个等待队列(即为每一个实例准备的线程休息室),当线程处于锁定状态时,其余线程须要等待获取这个锁时,会加入该对象的等待队列(即图中的EntryList),而后等待获取锁,即被Owner指针所指向,(若是此时被wait()方法挂起,则会进入WaitSet),若是中间没有被挂起过,则最后会调用monitorexit()方法释放锁,并将锁的计数器减一;java

  1. 那这些Owner指针,EntryList,WaitSet和锁的计数器究竟存储在哪里呢?实际上是存储在每一个实例对象都会对应的一个monitor对象里面,而重量级锁中(处在重量级锁状态时对象的MarkWord中)的指针即为指向这个对象,这个monitor对象 是由 ObjectMonitor()对象实现的
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
复制代码

此时就能够明白了,每一个对象对应的信息都是存储在这的;数组

  1. 此时要注意Owner这个变量,由于轻量级锁一样涉及到,在处于轻量级锁锁定时,MarkWord中的指针为持有该锁的线程的栈帧中创建的一个叫锁记录(Lock Record)的空间,这个空间中只存放初始MarkWord及Owner指针(能够看出轻量级锁在内存方面的开销也会小一些),若是当执行完同步代码块以后发现该指针已经变成指向monitor对象的指针时,说明有另外一个线程竞争了这个对象致使锁膨胀(在变成重量级锁前,竞争锁的对象会适当自旋给定的次数,避免频繁的线程挂起和唤醒由于Java虚拟机的线程是映射到操做系统核心态的线程的,因此每次对于线程的操做,都会须要将系统转至核心态,而这个开销是比较大的,而这也是重量级锁慢的主要缘由),因此后面等待锁的线程也要进入阻塞状态;

2.2 使用ReentrantLock实现公平锁和打断条件

2.2.1 与synchronized的区别:

  1. 等待可中断:即一个线程在等待获取锁的过程当中若是超过了必定的时间,这个线程能够选择放弃等待,改成处理其余事情,而synchronized不能够;
  2. 可实现公平锁:固然ReentrantLock也能够是非公平的,构造的时候能够选择;
  3. 锁能够绑定多个条件:能够和Condition配合使用;
  4. 性能:synchronized在JDK1.6优化后性能与ReentrantLock至关;
  5. 实现方式:synchronized是基于JVM实现的,ReentrantLock是基于AQS实现的;

2.2.2 使用场景

  1. 除非是须要使用ReentrantLock的高级特性,不然仍是使用synchronized比较好,首先是由于前者须要在finally代码块中手动释放锁,而JVM会保证后者的锁释放;其次是由于如今二者的性能也比较接近;

3、锁升级:

3.1 为何会出现这个机制?

  1. 这种方案的提出主要是基于大部分时候,并发环境下的线程竞争比较少,因此可使用乐观锁的想法去优化悲观锁的性能;

3.2 锁升级的过程:

  1. 偏向锁:当一个对象第一次被一个线程访问时,会将对象的MarkWord修改成偏向锁,并将该线程Id使用CAS标识在MarkWord中(这里为何要用CAS改,是为了防止两个线程同时修改MarkWord,致使线程冲突);因此偏向锁适用于预计只有一个线程访问的代码
  2. 轻量级锁:当另外一个线程去访问被偏向锁锁定的对象时发现MarkWord中的线程ID并非指向本身的,这个时候就会在安全点时Stop The Wrold,查看当前MarkWord中的线程是否还在运行,若是已经终止则将线程ID改成本身;若是以前的线程尚未中止运行,则须要解偏向锁,升级成轻量级锁;轻量级锁适用于保护的代码块执行速度很快,且预计不会发生线程冲突的场景
  3. 重量级锁:在多个线程竞争轻量级锁,而且自旋失败后,会升级成重量级锁。
相关文章
相关标签/搜索