【JavaEE】锁策略

目录

前言

一.悲观锁和乐观锁

二.重量级锁和轻量级锁

三.挂起等待锁和自旋锁

四.公平锁和非公平锁

五.可重入锁和不可重入锁

六.读写锁

synchronized对应的锁策略

1.悲观锁和乐观锁

2.重量级锁和轻量级锁

3.挂起等待锁和自旋锁

4.公平锁和非公平锁

5.可重入锁和不可重入锁

相关面试题

1.你是怎么理解乐观锁和悲观锁的,具体是怎么实现的?

2.介绍下读写锁?

3.什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?

4.synchronized是可重入锁吗?


前言

在前面线程安全中,我们介绍了在多线程中为什么要进行加锁,以及如何使用synchronized进行加锁,那么我可以再来了解一下有哪些锁策略,锁策略不仅仅局限于java,任何和锁有关的话题,都可能会涉及到我接下来讲解的这些。

一.悲观锁和乐观锁

悲观锁:认为发生锁冲突的概率比较大,总是假设最坏的情况,每次取拿数据的时候都认为别人会修改,所以在每次拿数据的时候都会加锁,这样别人想要拿到这个数据,就需要阻塞到它拿到锁为止。

乐观锁认为发生锁冲突的概率比较小,假设数据一般情况下不会发生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,就会返回用户错误的信息,让用户决定如何做。

【JavaEE】锁策略_第1张图片

悲观锁和乐观锁都是计算机对发生锁冲突的预测。这两个锁并不能说谁优谁劣,而是需要看当前的场景是否使用合适。

举个例子:假如你现在刚考完期末考试,对成绩进行预测。悲观锁的做法:对自己的成绩进行预测,发现自己在做题过程错太多了,导致最后的评估分数过低,那么就会对自己的努力产生怀疑,导致对自己的情绪影响较大。乐观锁的做法:在评估完成绩后,如果成绩过低,考虑自己是不是太粗心了,下次再注意点就行,对自身的情绪并不会有多大的影响。

二.重量级锁和轻量级锁

重量级锁和轻量级锁是站在加锁的开销角度来进行划分的。

重量级锁:依赖于操作系统的Mutex Lock(互斥锁)来实现的,因此状态切换涉及到用户态和内核态的切换,开销较大。适用于线程竞争激烈且锁持有时间长的场景,可以有效避免CPU资源浪费,但线程阻塞和唤醒涉及到系统调用,可能会导致性能问题,尤其是在高并发的环境下。

  • 大量的内核态用户态切换
  • 很容易引发线程调度

轻量级锁:加锁机制尽可能不适用mutex,而是尽量在用户态代码完成,实在搞不定再使用mutex。轻量级锁适用于线程交替执行同步代码块(即互斥操作),如果同一时刻多个线程同时访问同一个锁,则会导致轻量级锁变成重量级锁。

  1. 少量内核态用户态切换
  2. 不太容易引起线程调度

理解用户态 vs 内核态:(可以结合前面线程池讲到的例子线程池)

      想象去银行办业务,在窗口外, 自己办理相关业务, 这是用户态,用户态的时间成本是比较可控的。在窗口内, 工作人员做, 这是内核态,内核态的时间成本是不太可控的。如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的。

轻量级锁在没有锁竞争的情况下性能更优,而重量级锁在锁竞争激烈的环境下比较合适

JVM中的锁机制设计为一个升级过程:无锁 -> 偏向锁  -> 轻量级锁 -> 重量级锁,目的是为了在不同的竞争环境下提供合适的同步机制。

三.挂起等待锁和自旋锁

挂起等待锁是悲观锁和重量级锁的一种典型实现,是一种消极的锁策略。当线程无法获取到锁时,会主动让出CPU并进行等待队列中,直到锁被释放并收到通知后才重新参与线程调度。适用于锁持有时间不确定或者较长的情况,可以节省CPU资源,防止无效的循环等待

自旋锁是乐观锁和轻量级锁的一种典型实现,是一种积极的锁策略。当线程获取锁失败时,并不会让出CPU进行阻塞等待,而是会一直循环尝试获取到锁,直到拿到锁为止。这种优点在于当锁被释放之后,自旋线程能够立即获取到锁,提高了响应速度。但若锁被长时间占用,自旋线程将持续占用CPU资源,导致性能降低

在Java中,这两种锁策略的表现形式并不是直接作为API提供,而是JVM在实现锁机制时采取的策略。这两种锁的选用取决于具体的应用场景及需求。如果线程之间交替比较频繁且锁持有时间不长,那就可以使用自旋锁,若线程之间交互较少,且锁持有时间较长,那就可以使用挂起等待锁。

四.公平锁和非公平锁

公平锁:遵循先进先出原则的一种锁机制,当多个线程尝试获取同一个锁时,会按照线程请求锁的先后顺序来决定哪个线程可以获取到锁。在公平锁中,多个线程获取锁时,会先让等待最久的线程先获取到锁,不会有线程饿死的问题,确保了每个等待锁的线程最终都能获取到锁,提高了线程调度的公平性。但由于每次锁释放都需要唤醒队列中等待最久的线程,这会增加线程上下文切换的开销,特别是在高并发的场景下。

非公平锁:遵循概率均等原则的一种锁机制。非公平锁机制下不保证线程获取锁的顺序与请求锁的顺序是一致的,当锁被释放后,任何等待的线程都可以获取到锁,即使是刚开始等待的线程,也可能先获取到锁。这会导致部分线程长期获取不到锁,形成线程饿死。这种策略在某些场景下可以提高性能,不需要维护等待队列。

公平锁和非公平锁没有好坏之分,还是需要看具体的应用场景。在Java中,java.util.concurrent.locks.ReentrantLock类可以指定是否启用公平锁,通过构造函数传入true参数即可创建公平锁实例,否则默认为非公平锁Java的并发包中,公平锁强调的是线程获取锁的有序性和公平性,而非公平锁则倾向于提高系统的并发性能,但可能会使得部分线程在竞争锁时面临不公平待遇。

五.可重入锁和不可重入锁

可重入锁:指一个线程可以对同一把锁加多次锁,而不会引起死锁。说明当一个已经持有锁的线程再次进行请求获取同一把锁时,不会被阻塞,而是可以得到许可并继续执行。

可重入锁内部会维护一个计数器,记录当前线程获取到锁的次数,每获取到一次锁,计数器就会+1,每释放一次锁,计数器就会-1,当计数器为0时,锁才被真正释放。支持递归调用和嵌套同步,降低了死锁风险

不可重入锁:线程一旦获取到锁之后,若再次请求则会陷入阻塞,无法获取到锁。即一个线程不能再次获取自己已经持有的锁。不允许进行二次加锁,因此在递归调用或者嵌套同步的场景下容易死锁。

可重入锁相较与不可重入锁在除了多层同步和递归调用时具有更高的灵活性和安全性行,不易引发死锁问题。但在某些特定场景下,不可重入锁也可能因其简洁性和性能特点而被选用。并且Java中的synchronized 就是可重入锁.

六.读写锁

我们一般对数据就是进行读操作和写操作,在单线程中,读和写操作并不会产生线程安全问题,但是在多线程中可能就会出现一些线程安全问题,当一个线程在读数据,另一个线程在写数据,那么最后读取的数据和数据 本身不一致;若两个及以上的线程同时在进行写操作,最后得到的结果也不是正确的结果。而多个线程在读数据时是不会有线程安全问题的。那么如何解决这种情况呢?

读写锁因此产生,读写锁是一种比互斥锁更细粒度的锁,区分读操作和写操作允许多个线程同时进行读操作,但不允许同时进行写操作。若有多个线程同时进行读写操作或者同时进行写操作时,读写锁就是互斥的,后面的线程无法获取到锁,保证数据一致性

特点

  1. 读读不互斥:多个线程可以同时读取共享资源,读操作没有线程安全问题。
  2. 读写互斥:若一个线程读,另一个线程写,则会有线程安全问题。
  3. 写写互斥:若两个线程同时进行写操作,则也会有线程安全问题。

synchronized对应的锁策略

1.悲观锁和乐观锁

synchronized是自适应锁,初始时使用乐观锁策略,当发现锁竞争比较激烈时,就会自动切换成悲观锁。

2.重量级锁和轻量级锁

对于重量级锁和轻量级锁,synchronized也是属于自适应锁。开始的时候为轻量级锁,当发现锁竞争比较频繁时,就会切换为重量级锁。

3.挂起等待锁和自旋锁

对于挂起等待锁和自旋锁来说,synchronized也是自适应锁。当锁冲突较小时,synchronized就为自旋锁,当锁冲突较大时,就会切换为挂起等待锁。

4.公平锁和非公平锁

synchronized属于非公平锁。在多线程中,当多个线程尝试获取锁,synchronized不管先后顺序,而是所有竞争锁的线程一起竞争。

5.可重入锁和不可重入锁

synchronized属于可重入锁。当线程获取到锁后,此时若再对同一把锁加锁,不会陷入死锁状态。

通过维护一个计数器,当计数器为0时就释放锁。synchronized不是读写锁是互斥锁

相关面试题

1.你是怎么理解乐观锁和悲观锁的,具体是怎么实现的?

悲观锁认为多个线程访问同一个共享变量冲突的概率比较大,会在每次访问共享变量之前都真正加锁。

乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁,而是会尝试访问数据。在访问数据的同时识别当前数据是否出现访问冲突。

悲观锁的实现就是先加锁(比如借助操作系统提供的mutex),获取到锁再操作数据,获取不到锁就进行等待。

乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。

2.介绍下读写锁?

  • 读写锁就是把读操作和写操作分别进行加锁。
  • 读锁和读锁之间不互斥。
  • 读锁和写锁之间互斥。
  • 写锁和写锁之间互斥。
  • 读写锁最主要用在“频繁读,不判断写”的场景中。

3.什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?

如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能在第一时间获取到锁。

相当于挂起等待锁。

优点:没有放弃CPU资源,一旦锁被释放就能第一时间获取到锁,更高效,在锁持有时间比较短的场景下非常有用。

缺点:如果锁持有时间较长,就会浪费CPU资源。

4.synchronized是可重入锁吗?

是可重入锁。

可重入锁就是指在连续两次加锁不会导致死锁。

实现方式就是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数),如果发现当前加锁的线程就是持有锁的ixanc,则直接计数自增。


本篇就先到这里了~

若有不足,欢迎指正~

你可能感兴趣的:(java,java,开发语言,java-ee,经验分享,面试)