Java虚拟机对synchronized的优化
- 从Java6开始,虚拟机对synchronized关键字做了多方面的优化,主要目的就是,避免ObjectMonitor的访问,减少“重量级锁”的使用次数,并最终减少线程上下文切换的频率,
- 其中主要做了以下几个优化:锁自旋,轻量级锁,偏向锁
1.锁自旋
- 线程的阻塞和唤醒需要cpu从用户态转为核心态,频繁的阻塞和唤醒对cpu来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,所以Java引入了自旋锁的操作。
- 所谓自旋,就是让线程等待一段时间,不会被立即挂起,看当前持有锁的线程是否会很快释放锁。而所谓的等待就是执行一段无意义的循环即可(自旋)。
- 自旋锁也存在一定的缺陷:自旋锁要占用CPU,如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的cpu时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。
2.轻量级锁
-
在java虚拟机中存在这种情形,对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这个锁对象,也就是不存在锁竞争的情况。这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。
-
对象头中的Mark Word标记数据中,锁的标志包含如下:
- 00 代表轻量级锁
- 01 代表无锁(或者偏向锁)
- 10 代表重量级锁
- 11 则跟垃圾回收算法的标记有关
-
当线程执行某同步代码时,java虚拟机会在当前线程的栈帧中开辟一块空间(Lock Record)作为该锁的记录,如下图所示:
- 然后java虚拟机会尝试使用CAS操作,将锁对象的Mark Word拷贝到这块空间中,并且将锁记录中的owner指向Mork Word。结果如下:
- 当线程再次执行此同步代码块时,判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块,否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁
3.偏向锁
-
轻量级锁是在没有锁竞争情况下的锁状态,但是在有些时候不仅存在多线程的竞争,并且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。
- 偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占和释放锁的操作。
- 偏向锁可以通过 -XX:+UseBiasedLocking 开启或关闭
-
偏向锁的具体实现
- 在锁对象的对象头中有个ThreadId字段,默认情况下这个字段是空的,当第一次获取锁的时候,就将自身的ThreadId写入锁对象的Mark Word中的ThreadId字段内,将是否偏向锁的状态设置为01;
- 这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需要再次获取锁,略过了轻量级锁和重量级锁的加锁阶段,提高了效率。
总结
- Java锁的几种状态:
- 其中偏向锁和轻量级锁都是通过自旋等技术避免真正的加锁。
- 而重量级锁次啊是获取锁和释放锁,重量级锁通过对象内部的监视器(ObjectMonitor)实现,其本质依赖于底层操作系统的Mutex Lock实现
- 操作系统实现线程之间的切换需要从用户态到内核态的切换,成本非常高。
网友评论