Java高并发之锁总结、常见的面试问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u014454538/article/details/99172899

1. 锁的分类

① 乐观锁与悲观锁
  • 悲观锁:
  1. 对共享数据进行访问时,悲观锁总是认为一定会有其他线程修改数据。如果不加锁,肯定会出问题。
  2. 因此,悲观锁无论是否出现共享数据的争用,在访问数据时都会先加锁。
  • Java中同步互斥都是采用这种悲观的并发策略synchronized关键字Lock接口的实现类都是悲观锁。
  • 乐观锁:
  1. 对共享数据进行访问时,乐观锁总是认为不会有其他线程修改数据修改数据
  2. 于是直接执行操作,只是在更时检查数据是否已经被其他线程修改
  3. 如果没有被修改,则操作执行成功;否则,添加其他补偿措施。
  4. 常见的补偿措施是不断尝试,直到成功。
  • Java中的非阻塞同步都是采用这种乐观的并发策略,乐观锁在Java中是通过使用无锁编程来实现,最常使用的CAS操作
  • 比如,线程安全的原子类的自增操作,就是通过循环的CAS操作实现的。
    在这里插入图片描述
② 独占锁和共享锁
  • 独占锁:
  1. 又叫排它锁,同一个锁对象,同一时刻只允许一个线程获取到锁
  2. 如果线程T对数据A加上独占锁后,其他线程不能对该数据再加任何类型的锁(包括独占锁和共享锁),自己可以对数据进行读操作或者写操作。
  3. 独占锁允许线程对数据进行读写操作
  • Java中的 synchronized关键字MutexReentrantLockReentrantReadWriteLock写锁,都是独占锁。
  • 共享锁:
  1. 同一个所对象,同一时刻允许多个线程获取到锁
  2. 线程T对数据A加上共享锁,则其他线程只能对数据A加共享锁,不能加独占锁。
  3. 共享锁只允许对数据进行读操作
  • java中ReentrantReadWriteLock读锁是共享锁。
  • ReentrantReadWriteLock读写锁的获取:
  1. 同步状态不为0,如果有其他线程获取到读锁或者当前线程不是持有写锁的线程,则获取写锁失败进入阻塞状态;否则,当前线程是持有写锁的线程,直接通过setState()方法增加写状态。
  2. 同步状态为0,直接通过compareAndSetState()方法实现写状态的CAS增加,并将当前线程设置为持有写锁的线程。
  3. 如果有其他线程获取到了写锁,则获取读锁失败进入阻塞状态。
  4. 如果写锁未被获取或者该线程为持有写锁的线程,则获取读锁成功,通过compareAndSetState()方法实现读状态的CAS增加
  • 独占锁和共享锁都是通过AQS实现的,tryAcquire()或者tryAcquireShared()方法支持独占式或者共享式的获取同步状态。
③ 公平锁和非公平锁
  • 公平锁:
  1. 当锁被释放,按照阻塞的先后顺序获取锁,即同步队列头节点中的线程将获取锁。
  2. 公平锁可以保证锁的获取按照FIFO原则,但需要进行大量的线程切换,导致吞吐率较低
  • 非公平锁:
  1. 当锁被释放,所有阻塞的线程都可以争抢获取锁的资格,可能导致先阻塞的线程最后获取锁。
  2. 非公平锁虽然可能造成线程饥饿,但极少进行线程的切换,保证了更大的吞吐量
  • Java中ReentrantLockReentrantReadWriteLock支持公平和非公平访问,而synchronized关键字只支持非公平访问。
  • 公平与非公平可以通过构造函数的fair参数进行指定,默认是false,即默认为非公平的获取锁
  • 公平和非公平都是依靠AQS实现的,公平使用FairSync同步器,非公平使用NoFairSync同步器。
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
④ 可重入锁和非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

  • 可重入锁:
  1. 已经获取锁的线程再次获取该锁而不被锁所阻塞,需要解决线程再次获取锁锁的最终释放两个问题。
  2. 可重入锁可以一定程度的避免死锁
    在这里插入图片描述
  • 非可重入锁:
  1. 已经获取锁的线程再次获取该锁,会因为需要等待自身释放锁而被阻塞。
  2. 非可重入锁容易造成当前线程死锁,从而使整个队列中线程永久阻塞。
    在这里插入图片描述
  • Java中的synchronized关键字ReentrantLock锁ReentrantReadWriteLock锁支持重进入,其中ReentrantReadWriteLock读锁是支持重进入的共享锁写锁是支持重进入的独占锁
⑤ 无锁VS偏向锁VS轻量级锁VS重量级锁
  • synchronized关键字实现同步的基础是每个对象都是一个锁,它依靠对象头存储锁。
  • 无锁偏向锁轻量级锁重量级锁都是专门针对synchronized关键字设计的、级别从低到高的4种状态。
  • 注意: 锁状态只能升级不能降级
  • 对象头中的第一个字宽叫做Mark Word,用于存储对象的hashCode分代年龄等信息。
  • 其中最后2 bit的标志位,用于标记锁的状态。根据标志位的不同,可以有如下几种状态:
    在这里插入图片描述

无锁

  • 不对资源进行锁定,所有的线程都可以访问并修改同一资源,但同一时刻只有一个线程能修改成功。
  • 无锁的修改操作依靠循环实现: 如果没有争用,修改成功并退出循环;否则,循环尝试修改操作,直到成功。
  • 无锁无法全面代替有锁,但在某些场景下具有非常高的性能。
  • 无锁的经典实现: CAS操作。

偏向锁

  • 出现的原因:
  1. 在无竞争的情况下,同一线程可能多次进入同一个同步块,即多次获取同一个锁。
  2. 如果进入和退出同步块都使用CAS操作来加锁和解锁,则会消耗一定的资源。
  3. 于是通过CAS操作将线程ID存储到Mark Word中,线程再次进入或退出同步块时,直接检查Mark Word是否存储指向当前线程的偏向锁。如果存储了,则直接进入或退出同步块。
  • 偏向锁可以在无竞争的情况下,尽量减少不必要的轻量级锁执行路径。轻量级锁的加锁和解锁都需要CAS操作,而偏向锁只有将线程ID存储到Mark Word中时才执行一次CAS操作
  • 偏向锁的释放:
  1. 当有其他线程竞争偏向锁时,持有偏向锁的线程会释放锁偏向锁。
  2. 释放时,会根据锁对象是否处于锁定状态而恢复到不同的状态。
  3. 如果锁对象处于未锁定状态,撤销偏向后恢复到无锁的状态0 + 01 );如果锁对象处于锁定状态,撤销偏向后恢复到轻量级锁的状态00)。
  • 偏向锁在JDK1.6及以后,默认是启用的,即-XX:+UseBiasedLocking。可以通过-XX:-UseBiasedLocking关闭偏向锁。

轻量级锁

  • 多个线程竞争同步资源时,没有获取到资源的线程自旋等待锁的释放
  • 加锁过程:
  1. 线程进入同步块时,如果同步对象处于无锁状态0 + 01),JVM 首先在当前线程的栈帧中开辟一块叫做锁记录Lock Record)的空间,用于存储同步对象的Mark Word的拷贝。这个拷贝加了一个前缀,叫Displaced Mark Word
  2. 然后通过CAS操作将同步对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向同步对象的Mark Word
  3. 如果这个更新动作成功,则当前线程拥有了该对象的锁,Mark Word中的标志位更新为00表示对象处于轻量级锁定状态
  4. 如果更新动作失败,JVM首先会检查同步对象的Mark Word是否指向当前线程的栈帧。如果是,说明当前线程已经持有了该对象的锁,可以直接进入同步块继续执行;否则,说明存在多线程竞争锁。
  • 轻量级锁升级为重量级锁:
  1. 若当前只有一个线程在等待,则通过自旋进行等待。自旋超过一定的次数,轻量级锁升级为重量级锁。
  2. 若一个线程持有锁,一个线程自旋等待锁,又有第三个线程想要获取锁,轻量级锁升级为重量级锁。
  • 锁的释放:
  1. 通过CAS操作,将Lock Record中的Displaced Mark Word与对象中的Mark Word进行替换。
  2. 替换成功,同步状态完成;替换失败,说明有其他线程尝试获取过该锁,释放锁的同时需要唤醒被挂起的线程

重量级锁

  • 多线程竞争同步资源时,没有获取到资源的线程阻塞等待锁的释放
  1. 轻量级锁升级为重量级锁,锁的标志位变成10Mark Word中存储的是指向重量级锁的指针
  2. 所有等待锁的线程都会进入阻塞状态。
⑥ 自旋锁与自适应自旋锁
  • 自旋锁:
  1. 阻塞或唤醒一个线程都需要从用户态切换到内核态去完成,会对性能造成很大影响。
  2. 有时一个线程持有锁的时间很短,如果在很短的时间内让后续获取锁的线程都进入阻塞态,这是很不值得。
  3. 可以让后续线程持有CPU时间等待一会,这个等待需要执行忙循环(自旋) 来实现。
  4. 自旋等待的时间由自旋次数来衡量,默认为10,可以使用-XX:PreBlockSpin来进行设置。
  5. 如果在自旋等待中,持有锁的线程释放该锁,当前线程可以不必阻塞直接获取同步资源
  6. 如果超过自旋次数仍未获取成功,则使用传统的方法将其阻塞
  • 自旋锁的实现原理: 循环的CAS操作
  • 自旋锁的缺点:
  1. 自旋锁虽然避免了线程的切换开销,但是会占用CPU时间。
  2. 如果每个等待获取锁的线程总是自旋规定的次数,却又没有等到锁的释放,这样就白白浪费了CPU时间
  • 自旋锁在JDK1.4.2中引入,默认是关闭的;在JDK1.6中变成默认开启,并为了解决自旋锁中浪费CPU资源的问题,而引入了自适应自旋锁
  • 自适应自旋锁:
  1. 自适应意味着自旋的次数不再固定,而是根据上一次在同一个锁自旋的次数锁的拥有者的状态来决定。
  2. 如果在同一个锁对象上自旋刚刚成功获取过锁,并持有锁的线程处于运行状态,则可以认为这一次自旋也很可能成功,允许它自旋更长的时间。
  3. 如果在一个锁上,自旋很少成功,则下一次可以省略自旋过程,直接阻塞线程,避免浪费处理器资源。

2. 锁的有关问题总结

1. Java中的乐观锁与悲观锁

  • 基础: 什么是乐观锁,什么是悲观锁?二者的典型代表(非阻塞同步和互斥同步)
  • 进阶: CAS如何实现原子更新的;JUC包中的原子类如何实现线程安全的自增。

2. 一个线程怎么判断自己是否可以获得共享资源的锁?

  • Lock的实现类,线程在调用 lock()方法获取锁时,lock()方法会调用AQS中的模板方法,模板方法会调用AQS中的可重写方法。在可重写方法中,会通过判断同步状态,决定是否可以获取共享资源的锁。
  • 深入举例:
  1. ReentrantLock的公平和非公平获取锁:同步状态为0不为0的情况。
  2. ReentrantReadWriteLock写锁获取: 同步状态不为0为0的情况。
  3. ReentrantReadWriteLock读锁的获取: 不是持有写锁的线程,获取失败;其他情况获取成功。

3. 共享资源state何时增加何时减少?对于synchronized和lock有什么区别?

  • 加锁state的值加1,解锁state的值减1,state = 0 表示锁被释放。
  • synchronized和lock都支持重进入:lock依靠AQS中的同步状态synchronized依靠对象头中Mark Word的锁标志位处于可偏向状态1 + 01)。

4. 对于++i操作,如何不使用锁进行同步,保证其线程安全?

  • JUC包中的原子类的自增运算(incrementAndGet()或者compareAndSet()),实质: 循环的CAS操作。

5. CAS操作的实现机制?

  • 基础: 如何实现原子更新的、Java中对CAS的支持
  • 进阶:
  1. 如何使用CAS操作实现非同步互斥,以原子类的自增运算为例。
  2. CAS操作存在的问题:ABA问题、循环时间过长消耗大、只能保证一个共享变量的原子操作。

6. 公平锁与非公平锁

扫描二维码关注公众号,回复: 7659787 查看本文章
  • 基础: 什么是公平锁,什么是非公平锁。
  • 进阶:
  1. synchronized只支持非公平访问, ReentrantLockReentrantReadWriteLock支持公平与非公平访问。
  2. ReentrantLock为例,讲解如何支持公平与非公平的:构造函数中指定fair参数,对应的AQS为FairSync或者NonfairSync,公平访问要求没有前驱节点。

7. lock ,sychronized,volatile的区别

  • lock的特性: 非阻塞获取锁、可中断获取锁、超时获取锁;lock接口的几种方法;常见的Lock的实现类。
  • synchronized的同步基础,如何实现同步。
  • volatile保证变量在线程间的可见性禁止指令重排序。只适合运算的结果不依赖当前变量值得情况,无法解决线程同步。
  • 总结:sychronizedReentrantLock为例,二者的异同:实现、性能、是否可中断、公平性支持、是否可绑定多个对象。

  • lock和synchronized的区别:
  1. 实现: lock由jdk支持,synchronized由JVM支持;导致前者需要显式的获取、释放锁;后者由JVM隐式完成。
  2. 性能: synchronized进行了很多的优化,目前二者的性能大致相同。
  3. lock的特性: 支持非阻塞获取(tryLock)、中断获取(lockinterruptibly)、超时获取(带参数的tryLock),而synchronized不支持。
  4. 二者的实现机制: synchronized依靠进入和退出monitor对象,实现同步方法或同步代码块;lock依靠AQS实现锁的获取和释放。
  5. 线程间的协作: synchronized依靠Object的监视器方法wait()notify()/notifyAll(),实现等待/通知模式;lock接口的实现类,依靠绑定的condition对象的await()signal()/signalAll(),实现等待/通知模式。

8. 谈一下Java中的锁

  • 锁是通过互斥同步实现Java线程安全的一种手段,JVM以synchronized关键字提供锁功能,JDK1.5及以后提供依靠Lock及其实现类提供锁功能。
  • 讲解方法一:
  1. synchronized关键字:实现同步的基础,锁存储的位置、如何实现同步。[ 如何保证原子性、可见性、顺序性 ]
  2. Lock接口: 如何借助AQS实现锁;ReentrantLock的重进入、公平与非公平;ReentrantReadWriteLock的写锁/读锁的获取。
  3. synchronizedReentrantLock的比较与选择
  4. 锁的优化:自旋锁与自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
  • 讲解方法二:
  1. 独占锁与共享锁公平锁与非公平锁可重进入锁与非可重进入锁乐观锁与悲观锁自旋锁与自适应自旋锁、无锁VS偏向锁VS轻量级锁VS重量级锁,它们的含义、具体的实例等

9. synchronized关键字和ReentrantLock

  • 基础: synchronized的同步基础、锁存储的位置、如何实现同步;ReentrantLock的特性重进入、公平与非公平
  • 进阶: 二者的比较与选择

10. 共享锁与独占锁

  • 独占锁: 一个线程持有锁,其他线程不能再获取锁,无论执行读操作还是写操作
  • 共享锁: 一个线程持有读锁,其他线程可以在获取读锁,不能再获取写锁

11. 单例模式如何解决高并发?

  • DCL(双重锁检测)的单例模式:instance定义为volatile,使用synchronized再次进行instancenull的判断。
  • 重点: 两次null判断的意义,添加volatile关键字的意义。

12. AQS和condition对象的关系

  • ConditionObject是AQS的内部类,一个AQS不仅拥有自身的同步队列,还拥有 ConditionObject的等待队列;而且,一个AQS可以绑定多个condition对象。
  • 调用condition对象的await()方法时,要求当前线程已经获取到锁。即该线程处于同步队列的头节点中。接着当前线程释放该并将自己封装成新的节点加入到等待队列中,进入等待状态;从AQS的角度看,调用await()方法时,节点从同步队列的头节点移动到等待队列的尾节点
  • 调用condition对象的signal()方法时,会唤醒处于等待队列中等待最久的节点,即等待队列的头节点;线程想从await()方法返回,必须获取到锁,因此线程又会封装成同步节点,加入到同步队列中。从AQS的角度看,调用signal()方法时,节点从等待队列的头节点移动到同步队列的尾节点

13. lock接口的实现

  1. 定义lock接口的实现类,在其中创建AQS的子类(静态内部类)作为同步器;
  2. 实现AQS的子类: 调用更改同步状态的三种核心方法,实现AQS中提供的可重写方法。
  3. 实现lock接口中的方法: 调用同步器中的模板方法,实现lock接口的lock()unlock()等方法。

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/99172899