通过synchronized来看Java锁机制

一、何为锁住一个对象?

  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.1 非公平性:

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

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

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

  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.1 为什么会出现这个机制?

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

3.2 锁升级的过程:

  1. 偏向锁:当一个对象第一次被一个线程访问时,会将对象的MarkWord修改为偏向锁,并将该线程Id使用CAS标识在MarkWord中(这里为什么要用CAS改,是为了防止两个线程同时修改MarkWord,导致线程冲突);所以偏向锁适用于预计只有一个线程访问的代码
  2. 轻量级锁:当另一个线程去访问被偏向锁锁定的对象时发现MarkWord中的线程ID并不是指向自己的,这个时候就会在安全点时Stop The Wrold,查看当前MarkWord中的线程是否还在运行,如果已经终止则将线程ID改为自己;如果之前的线程还没有停止运行,则需要解偏向锁,升级成轻量级锁;轻量级锁适用于保护的代码块执行速度很快,且预计不会发生线程冲突的场景
  3. 重量级锁:在多个线程竞争轻量级锁,并且自旋失败后,会升级成重量级锁。

猜你喜欢

转载自juejin.im/post/5e3d0fabe51d4526e418f0f4