共享锁排他锁重入锁锁的公平与非公平

Posted 架构师之巅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了共享锁排他锁重入锁锁的公平与非公平相关的知识,希望对你有一定的参考价值。

共享锁

共享锁又称读锁,允许多个线程同时获取锁,并发访问共享资源。在Java中CountDownLatch是一种共享锁的实现。

排他锁

排他锁又称写锁(独占锁),独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

重入锁

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
我们来看一段代码:

 1public class Demo{
2    Lock lock = new Lock();
3    public void outer(){
4        lock.lock();
5        inner();
6        lock.unlock();
7    }
8    public void inner(){
9        lock.lock();
10        //do something
11        lock.unlock();
12    }
13}

outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入 。通常也称为 自旋锁 。相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

可重入锁最大的作用是避免死锁。

自旋锁

自旋锁与互斥锁相似,基本作用是用于线程(进程)之间的同步。与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞);试想下,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程B可以不放弃CPU时间片,而是在“原地”忙等,直到锁的持有者释放了该锁,这就是自旋锁的原理,可见自旋锁是一种非阻塞锁。

下面看下JAVA自旋锁的简单实现:

 1public class SpinLock {
2    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象
3    private int count;//用一个计数器 来做 重入锁获取次数的计数
4    public void lock({
5        Thread cur = Thread.currentThread();
6        if (cur == owner.get()) {
7            count++;
8            return;
9        }
10
11        while (!owner.compareAndSet(null, cur)) {
12//当线程越来越多  由于while循环 会浪费CPU时间片,CompareAndSet 需要多次对同一内存进行访问
13            //会造成内存的竞争,然而对于X86,会采取竞争内存总线的方式来访问内存,所以会造成内存访问速度下降(其他线程老访问缓存),因而会影响整个系统的性能
14        }
15    }
16
17    public void unLock({
18        Thread cur = Thread.currentThread();
19        if (cur == owner.get()) {
20            if (count > 0) {
21                count--;
22            } else {
23                owner.compareAndSet(cur, null);
24            }
25        }
26    }
27}

自旋锁可能引起的问题:

  1. 过多占据CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞;

  2. 死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

锁的公平与非公平

公平锁:

  1. 公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。

  2. 锁的实现方式是基于如下几点:
     1). 表结点Node和状态state的volatile关键字。
     2). sum.misc.Unsafe.compareAndSet的原子操作。

非公平锁:

在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。

非公平锁性能高于公平锁性能的原因:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。

假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。

当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。

ReentrantLock 公平锁与非公平锁的创建

默认的构造器是非公平锁:

1public ReentrantLock() {
2
3       sync = new NonfairSync();
4
5}

公平锁的构造器如下:

1public ReentrantLock(boolean fair) {
2          sync = fair ? new FairSync() : new NonfairSync();
3    }



                                               


以上是关于共享锁排他锁重入锁锁的公平与非公平的主要内容,如果未能解决你的问题,请参考以下文章

Java高并发学习—— Java锁

Java 独占锁与共享锁公平锁与非公平锁可重入锁

java单体锁分类

JUC源码学习02重入锁(ReentrantLock)学习

Java锁之重入锁(Reentrantlock)原理,公平锁与非公平锁

可重入的独占锁ReentrantLock概述-公平与非公平锁的实现