一、何为锁住一个对象?
- 我们经常说当使用
synchronized 修饰一个代码块时
,编译后会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令
,而当虚拟机执行monitorenter指令时,会去尝试获取对象的锁,如果这个对象没有被锁定或者当前线程已经持有了这个对象的锁(可重入性),就会把锁的计数器加一,而在执行monitorexit指令时锁的计数器就会减一; - 此时就很奇怪,我自己定义的对象哪来的锁呢?其实是因为在堆中存储的每个对象虚拟机都会为其生成一个对象头,对象头一般分为两部分(如果是数组对象则会分为三部分),而对这个问题最重要的是第一部分(
Mark Word
),一般为32bit或64bit(由虚拟机的位数决定),当该对象处于未被锁定的状态时,MarkWord中有25bit用来存储对象的hashCode(这里猜测是为了便于找到对象?与HashMap类似,但没有深入去了解),4bit用于存储对象的分代年龄(GC相关),2bit用于存储锁标志位,1bit默认为0;
- 总结,当程序需要锁定一个对象时,所谓锁住一个对象即为改变该对象的MarkWord状态,让其他访问该对象的线程了解到这个对象处于被锁定的状态。
二、当锁住一个对象的时候有多个线程在等该如何去抉择?
2.1 非公平性:
- synchronized是非公平锁,即它不会按照线程等待的时间或其他因素排除优先级,而是当持有锁的对象释放锁后由一个随机的对象去获得锁;
每个实例对象都会拥有一个等待队列(即为每个实例准备的线程休息室),当线程处于锁定状态时,其他线程需要等待获取这个锁时,会加入该对象的等待队列(即图中的EntryList),然后等待获取锁,即被Owner指针所指向,(如果此时被wait()方法挂起,则会进入WaitSet
),如果中间没有被挂起过,则最后会调用monitorexit()方法释放锁,并将锁的计数器减一;
- 那这些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 ;
}
复制代码
此时就可以明白了,每个对象对应的信息都是存储在这的;
- 此时要注意Owner这个变量,因为轻量级锁同样涉及到,在处于轻量级锁锁定时,MarkWord中的指针为持有该锁的线程的栈帧中建立的一个叫锁记录(Lock Record)的空间,这个空间中只存放初始MarkWord及Owner指针(可以看出轻量级锁在内存方面的开销也会小一些),如果当执行完同步代码块之后发现该指针已经变成指向
monitor对象
的指针时,说明有另一个线程竞争了这个对象导致锁膨胀(在变成重量级锁前,竞争锁的对象会适当自旋给定的次数,避免频繁的线程挂起和唤醒,因为Java虚拟机的线程是映射到操作系统核心态的线程的,所以每次对于线程的操作,都会需要将系统转至核心态,而这个开销是比较大的,而这也是重量级锁慢的主要原因),所以后面等待锁的线程也要进入阻塞状态;
2.2 使用ReentrantLock实现公平锁和打断条件
2.2.1 与synchronized的区别:
- 等待可中断:即一个线程在等待获取锁的过程中如果超过了一定的时间,这个线程可以选择放弃等待,改为处理其他事情,而synchronized不可以;
- 可实现公平锁:当然ReentrantLock也可以是非公平的,构造的时候可以选择;
- 锁可以绑定多个条件:可以和Condition配合使用;
- 性能:synchronized在JDK1.6优化后性能与ReentrantLock相当;
- 实现方式:synchronized是基于JVM实现的,ReentrantLock是基于AQS实现的;
2.2.2 使用场景
- 除非是需要使用ReentrantLock的高级特性,否则还是使用synchronized比较好,首先是因为前者需要在
finally代码块中
手动释放锁,而JVM会保证后者的锁释放;其次是因为现在两者的性能也比较接近;
三、锁升级:
3.1 为什么会出现这个机制?
- 这种方案的提出主要是基于大部分时候,并发环境下的线程竞争比较少,所以可以使用乐观锁的想法去优化悲观锁的性能;
3.2 锁升级的过程:
- 偏向锁:当一个对象第一次被一个线程访问时,会将对象的MarkWord修改为偏向锁,并将该线程Id使用CAS标识在MarkWord中(这里为什么要用CAS改,是为了防止两个线程同时修改MarkWord,导致线程冲突);所以偏向锁适用于预计只有一个线程访问的代码
- 轻量级锁:当另一个线程去访问被偏向锁锁定的对象时发现MarkWord中的线程ID并不是指向自己的,这个时候就会在安全点时
Stop The Wrold
,查看当前MarkWord中的线程是否还在运行,如果已经终止则将线程ID改为自己;如果之前的线程还没有停止运行,则需要解偏向锁,升级成轻量级锁;轻量级锁适用于保护的代码块执行速度很快,且预计不会发生线程冲突的场景 - 重量级锁:在多个线程竞争轻量级锁,并且自旋失败后,会升级成重量级锁。