平时不用面试又爱问系列之锁优化问题

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了平时不用面试又爱问系列之锁优化问题相关的知识,希望对你有一定的参考价值。

前面我们提到了锁策略问题,那么锁又如何优化的呢?

1.锁的实现原理

既然要知道锁的优化问题,首先的知道锁的实现原理(以加锁为例):

  1. 先通过原子操作,检查并更新(CAS完成)一块内存区域,如果修改成功过,则表示加锁成功,否则表示加锁失败(更新的内存区域可以想象成哪个线程持有了这个锁)(用户/内核实现)
  2. 如果更新失败。表示锁被别的线程占用了(用户/内核实现)
  3. 如果被占用了,就挂起等待或者自旋或者…(用户/内核实现)
  4. 如果是挂起等待锁,就把该线程放到一个锁对应的等待队列中,后面锁释放了之后才有机会被唤醒(内核实现)
  5. 放弃CPU(内核实现)

如果前三步是用户实现了,就相当于轻量级锁,如果五步都是内核实现,那么就是重量级锁

2.JVM实现sychronized时的优化策略

大概步骤

  1. 编译器+JVM智能判定该所是否可以消除,如果可以,就直接消除,就不加锁
  2. 第一个获取线程的锁,那么就只加一个偏向锁(就是一个简单的标记位,没有互斥机制,如果有多个线程在竞争锁,那么就真正的加锁)
  3. 当多个线程进行竞争锁的时候,刚才的偏向锁就被消除,进入轻量级状态,没有获取得到锁的线程就会自旋等待
  4. 如果竞争激烈(既锁冲突很频繁),通过自旋锁也无法获取到锁,此时膨胀为重量级锁,在内核中针对该线程挂起瞪大
  5. 如果一段逻辑中,出现多次加锁解锁的操作,编译器+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.面试常考

  1. 什么是偏向锁
  2. java 的 synchronized 是怎么实现的,有了解过么?
  3. synchronized 实现原理 是什么?

以上是关于平时不用面试又爱问系列之锁优化问题的主要内容,如果未能解决你的问题,请参考以下文章

BATJ都爱问的多线程面试题

java并发优化之锁lock

java并发优化之锁lock

面试 HTTP ,99% 的面试官都爱问这些问题

高频面试java高级进阶之锁?与CAS详解#yyds干货盘点#

Java高并发之锁优化