Java的线程是映射到操作系统原生线程之上的,如果需要阻塞或唤醒一个线程 就需要操作系统的帮忙,这就要从用户态转换到核心态 ,状态转换需要花费很多的处理器时间。
private Object lock = new Object();
private int value;
public void setValue(){
sync(this){
value++;
}
}
value++因为被关键字sync修饰,所以会在各个线程间同步执行。但是value++消耗的时间很有可能比线程状态转换消耗的时间还短 ,所以说sync 是Java中一个重量级操作 。
要了解sync的原理,需要先理清楚两件事:1.对象头 2.Monitor
Java对象 在内存中的布局分为3部分:
当我们在Java代码中,使用new创建一个对象时,JVM会在堆中 创建一个instanceOopDesc对象 ,这个对象中包含了对象头和实例数据。
instanceOopDesc的基类为oopDesc类,主要结构:
volatile markOop _mark;
union _meradata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
其中_mark和 _metadata一起组成了对象头。 _metadata主要保存了类元数据。重点看下 _mark属性, _mark是markOop类型数据,一般称他为标记字段(Mark Word),其中主要存储了对象的hasCode,分代年龄,锁标志位,是否偏向锁等。
Monitor可以把他理解为一个同步工具,也可以描述为一种同步机制。实际上,他是一个保存在对象头中的一个对象。因此Java中每个对象都会有一个对应的ObjectMonitor对象,这也是Java中所有的Object都可以作为锁对象的原因。
那么ObjectMonitor是如何实现同步机制的呢?
ObjectMonitor结构中有几个比较关键的属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于 wait 状态的线程队列
_EntryList:存放处于等待锁 block 状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程通过竞争获取到对象的monitor后,monitor会把 _owner变量设置为当前线程,同时monitor中的计数器 _count加1,即获得对象锁。
若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null, _count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。
若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
private Object lock = new Object();
public void setValue(){
sync(lock){
//...
}
}
锁对象是lock对象,在JVM中会有一个ObjectMonitor对象与之对应。
实际上,ObjectMonitor的同步机制是JVM对操作系统级别的Mutex Lock(互斥锁)的管理过程,期间都会转入到OS内核态。也就是说sync实现锁,在“重量级锁”状态下,当多个线程之间切换上下文时,还是一个比较重量级的操作。
从Java6开始,虚拟机对sync关键字做了多方面优化,主要目的就是,避免ObjectMonitor的访问,减少“重量级锁”的使用次数,并最终减少线程上下文切换的频率 。主要做了一下几个优化:锁自旋 ,轻量级锁,偏向锁。
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,所以Java引入自旋锁的操作。实际自旋锁在Java1.4就被引入,只不过默认关闭。但在Java6后默认开启。
所谓自旋,就是让该线程等待一段使时间,不会被立即挂起,看当前持有锁的线程是否很快释放锁 。而所谓的等待就是执行一段无意义的循环即可(自旋)
自旋锁也存在一定缺陷:自旋锁需要占用CPU,如果锁竞争的时间比较长,那么自旋通常不能获得锁,浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景,此时应禁用自旋锁 。
有时Java虚拟机中会存在这种情况:对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也即是说不存在锁竞争 的情况。这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。
上面说到,Mark Word中,锁的标志位包含几种情况:00轻量级锁,01代表无锁(或偏向锁),10代表重量级锁,11和垃圾回收算法标记有关。
当线程执行某同步代码时,Java虚拟机会在当前线程的栈帧 中开辟一块空间(Lock Record) 作为该锁的记录 。
然后Java虚拟机会尝试使用CAS(compare and swap)操作,将锁对象的Mark Word拷贝到这块空间中,并将锁记录中的owner指向Mark Word。
当线程再次执行此同步代码块时,判断当前对象的Mark Word是否指向当前线程的栈帧,是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则说明该锁已经被其他线程抢占,这时轻量级锁需要膨胀为重量级锁。
轻量级锁适应的场景是线程交替执行同步代码块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
轻量级锁是在没有锁竞争情况下的锁状态,但是在有些时候不仅存在多线程的竞争,而且总是由同一个线程获得 。因此为了让线程获得锁的代价更低,引入了偏向锁。
偏向锁:如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或退出同一同步代码块,不需要再次进行抢占和释放锁的操作。
可以通过-XX:+UseBiasedLocking开启或关闭。
偏向锁的具体实现:在锁对象的对象头中有个ThreadId字段,默认情况下这个字段是空的,当第一次获取锁的时候,就将自身的ThreadId写入锁对象的Mark Word中的ThreadId字段内,将是否偏向锁的状态置为01。
这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致则认为当前线程已经获取了锁,因此不需要再次获取锁,略过了轻量级锁和重量级锁的加锁阶段 ,提高了效率。
其实偏向锁并不适合所有应用场景,因为一旦出现锁竞争,偏向锁会被撤销,并膨胀为重量级锁,而撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的sync块时,才能体现出明显改善。