平时不用面试又爱问系列之锁优化问题
Posted 你这家伙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了平时不用面试又爱问系列之锁优化问题相关的知识,希望对你有一定的参考价值。
前面我们提到了锁策略问题,那么锁又如何优化的呢?
1.锁的实现原理
既然要知道锁的优化问题,首先的知道锁的实现原理(以加锁为例):
- 先通过原子操作,检查并更新(CAS完成)一块内存区域,如果修改成功过,则表示加锁成功,否则表示加锁失败(更新的内存区域可以想象成哪个线程持有了这个锁)(用户/内核实现)
- 如果更新失败。表示锁被别的线程占用了(用户/内核实现)
- 如果被占用了,就挂起等待或者自旋或者…(用户/内核实现)
- 如果是挂起等待锁,就把该线程放到一个锁对应的等待队列中,后面锁释放了之后才有机会被唤醒(内核实现)
- 放弃CPU(内核实现)
如果前三步是用户实现了,就相当于轻量级锁,如果五步都是内核实现,那么就是重量级锁
2.JVM实现sychronized时的优化策略
大概步骤:
- 编译器+JVM智能判定该所是否可以消除,如果可以,就直接消除,就不加锁
- 第一个获取线程的锁,那么就只加一个偏向锁(就是一个简单的标记位,没有互斥机制,如果有多个线程在竞争锁,那么就真正的加锁)
- 当多个线程进行竞争锁的时候,刚才的偏向锁就被消除,进入轻量级状态,没有获取得到锁的线程就会自旋等待
- 如果竞争激烈(既锁冲突很频繁),通过自旋锁也无法获取到锁,此时膨胀为重量级锁,在内核中针对该线程挂起瞪大
- 如果一段逻辑中,出现多次加锁解锁的操作,编译器+JVM就会自动把多个相邻的加锁解锁合并成一次加锁解锁(锁粗化)
具体分析
2.1锁消除
就是在实际过程中,为了代码的可靠,在很多地方都会加锁,于是可能就会让一些本不应该加锁的地方也给加锁了 比如: StringBuffer 和 StringBuilder的区别:StringBuffer 内置了同步机制,也就是自己内部带锁StringBuffer stringBuffer = new StringBuffer();
//每次进行append的时候都会先加锁,操作完成了之后再解锁
stringBuffer.append("hello");
stringBuffer.append("world");
此时只有一个线程,不需要加锁,但是因为内部自带锁,所以编译器+JVM就会自动把stringBuffer中的加锁操作去掉了
2.2偏向锁
如果现在某个线程加锁,JVM不确定这个锁会不会出现多线程竞争锁的问题,那么他就会先把他当成不会京城的线程,就会先将这个线程进行标记
如果后面的线程不会和这个线程发生锁竞争的话,那就可以彻底不用给这个线程进行加锁
如果会和后面的线程发生竞争,这个时候才会去真正加锁
(因为加锁是开销比较大的,能不加就不加)
2.3自旋锁
- 自旋锁其实是耗着CPU在空转
- sychronized中的自旋是一种“自适应”的自旋锁。
- JVM会评估当前某个锁的竞争激烈度。
因为他是否自旋,以及自旋多久,都是“自适应”,会根据环境的差异,会发生一些变化,如果线程竞争锁比较激烈的时候,那么让他少自旋,自旋的时间短一点;如果线程竞争锁不是那么激烈,就让自旋锁自旋的频率高一些,时间久一点;要么就是变成重量级锁,让内核去完成,挂起等待就行了
2.4膨胀锁
这个锁其实是无奈之举,因为自旋锁的效率是特别高的,能够个更快的获取到锁,但是如果自旋了半天仍然获取不到锁,获取不到锁的期间有不能什么也不干,所以就膨胀为重量级锁,只能在内核中挂起等待了。
2.5锁粗化
就是把多个锁合并到一起,那么就会有锁的粒度
加锁解锁中间夹的代码越多,就认为锁的粒度越粗,反之越细(锁粒度越细,多线程并发执行时,并发程度越高)
注意:
- 如果锁竞争激烈,此时锁粒度越细越好
- 如果锁竞争不激烈
此时反复多次加锁解锁,不如一次加锁解锁来的更快
3.synchronized 锁的几种锁的状态
-
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
-
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
-
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
-
重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
4.面试常考
- 什么是偏向锁
- java 的 synchronized 是怎么实现的,有了解过么?
- synchronized 实现原理 是什么?
以上是关于平时不用面试又爱问系列之锁优化问题的主要内容,如果未能解决你的问题,请参考以下文章