Java面试高频面试题汇总 锁专题

时间:2022-07-25
本文章向大家介绍Java面试高频面试题汇总 锁专题,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

可重入锁(递归锁)

广义上可重入锁,也叫做递归锁,指的是同一线程,外层函数获得锁之后,内层函数仍有获得该锁的代码,但不受影响。Java的ReentrantLock和synchronized都是可重入锁。

公平锁和非公平锁

公平锁(Fair) 加锁前检查是否有排队等待的线程,优先排队等待的线程,先到先得。

非公平锁(Nonfair) 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

  1. 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。
  2. Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。

ReadWriteLock 读写锁

为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞的,在一定程度上提供了程序的执行效率。

读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由JVM控制的,你只需要上好相应的锁就可以了。

读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。 写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。

Java读写锁有对应的接口java.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock

共享锁和独占锁

java并发包提供的加锁模式分为共享锁和独占锁。 独占锁 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

共享锁 共享锁允许多个线程同时获取锁,并访问共享资源如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

  1. AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别表示AQS队列中等待线程的锁获取模式。
  2. java的并发包提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问。

重量级锁(Mutex lock,英文直译互斥锁)

Synchronized是通过对象内部的一个叫做监视器(monitor)锁来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock。而操作系统实现线程之间的切换就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁称之为重量级锁。JDK中对Synchronized做的中中优化,其核心都是为了减少这种重量级锁的使用。JDK1.6之后。为来减少获得锁和释放锁带来的性能消耗,引入了“轻量级锁”和“偏向锁”。

轻量级锁

锁的状态一种有四种:无锁状态、偏向锁、轻量级锁和重量级锁。 随着锁的竞争,锁可以从偏向锁升到轻量级锁,再升级到重量级锁(锁的升级是单向的,只能从低到高,不会降级)。

“轻量级”是相对于操作系统互斥量实现传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。 轻量级锁所适用的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。

偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

分段锁

ConcurrentHashMap,JDK7即之前实现同步的一种方式,对整个数组分段加锁,不是一种实际的锁。

锁优化

  • 减少锁持有时间 只在有线程安全要求的程序上加锁
  • 减小锁粒度 将大对象(指这个对象可能会被多线程访问)拆成小对象,大大增加并行度,降低锁竞争,降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
  • 锁分离 最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。
  • 锁粗化 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源之后,应该立即释放锁。但是,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于与性能的优化。锁粗化具体方法是将多个锁请求合并为一个。
  • 锁消除 锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员代码不规范引起的。