面试必备|Java中的公平锁和非公平锁,可重入锁,自旋锁

Posted JAVA炭烧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试必备|Java中的公平锁和非公平锁,可重入锁,自旋锁相关的知识,希望对你有一定的参考价值。

点赞再看,养成习惯
##公平锁和非公平锁
这里主要体现在ReentrantLock这个类里面了 公平锁、非公平锁的创建方式:

//创建一个非公平锁,默认是非公平锁

Lock lock = new ReentrantLock();

Lock lock = new ReentrantLock(false);

//创建一个公平锁,构造传参true

Lock lock = new ReentrantLock(true);

相关源码:

public ReentrantLock() {

sync = new NonfairSync();

}

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

————————————————
公平锁 是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞, CPU唤醒阻塞线程的开销比非公平锁大.

对于非 公平锁 ,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的 lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

再进入 hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况

##可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

先写个 demo大致的看一下:

public class AtomicDemo {

 public static void main ( String [] args ) {

     person person = new person () ;

for ( int i = 0; i < 10; i++ ) {
new Thread (){
@Override
public void run () {
person.aaa () ;

}
} .start () ;

}
}
}
class person {

 public synchronized void aaa (){
     System. out .println ( Thread. currentThread () .getName () + "aaa" ) ;

this.bbb () ;

}
public synchronized void bbb (){
System. out .println ( Thread. currentThread () .getName () + “bbb” ) ;

}
}

结果 :

Thread-1aaa

Thread-1bbb

Thread-2aaa

Thread-2bbb

Thread-0aaa

Thread-0bbb

Thread-3aaa

Thread-3bbb

Thread-4aaa

Thread-4bbb

Thread-5aaa

Thread-5bbb

Thread-7aaa

Thread-7bbb

Thread-6aaa

Thread-6bbb

Thread-9aaa

Thread-9bbb

Thread-8aaa

Thread-8bbb

说明 :

在上面的代码中,类中的两个方法都是被内置锁 synchronized修饰的,aaa()方法中调用bbb()方法。因为内置锁是可重入的,所以同一个线程在调用bbb()时可以直接获得当前对象的锁,进入bbb()进行操作。

如果是一个不可重入锁,那么当前线程在调用bbb()之前需要将执行aaa()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。


##自旋锁
先说说概念

阻塞或唤醒一个 Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是 CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

看看源码中的 CAS方法:

简单的说一下参数 :

Var1---->this( 也就是当前的对象 )

Var2----> 当前对象的主内存地址

Var4----> 要加的值

Var5----> 自旋完成后返回的值

再说细一点 :

var5 = this.getIntVolatile(var1, var2);

这个方法的就是在内存中找到 var1 对象的内存地址为 var2 的值赋值给 var5

再去判断 :

this.compareAndSwapInt(var1, var2, var5, var5 + var4)

当前 var5 的值与获取到主内存中的值是不是一样的 , 一样的就进行 var4+var5 的操作 , 返回 true, 否则就不做操作
#最后
码字不易,如果觉得本篇文章对你有用的话,请给我点赞!关注作者,后续会有更多的干货分享,请持续关注!

上一期我们讲了关于乐观锁与悲观锁的大揭秘,这一次简单讲了一下公平锁和非公平锁;如果你还想了解更多锁相关的知识,{可以关注笔者,或者添加笔者小助理: }获取更多锁相关笔记,彻底吃透锁相关原理

~联系方式~
wechat: Mlzg5201314zz
扣扣:2967728282///

上篇链接:[乐观锁与悲观锁的大揭秘](https://blog.csdn.net/m0_57227221/article/details/116540241?spm=1001.2014.3001.5501)

以上是关于面试必备|Java中的公平锁和非公平锁,可重入锁,自旋锁的主要内容,如果未能解决你的问题,请参考以下文章

第237天学习打卡(知识点回顾 公平锁和非公平锁 可重入锁 自旋锁 读锁和写锁)

什么是重入锁和AQS

Java的锁:公平锁,非公平锁,可重入锁,自旋锁,独占锁(写锁) / 共享锁(读锁) / 互斥锁...

重入锁

JUC之LockReentrantLock可重入独占锁

分布式锁02-使用Redisson实现公平锁原理