十四、并发编程之ReentrantLock公平锁和非公平锁原理详解

编程入门 行业动态 更新时间:2024-10-07 07:28:25

十四、并发编程之ReentrantLock<a href=https://www.elefans.com/category/jswz/34/1754813.html style=公平锁和非公平锁原理详解"/>

十四、并发编程之ReentrantLock公平锁和非公平锁原理详解

一、简介

Java语言中有许多原生线程安全的数据结构,比如ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue,它们线程安全的实现方式并非通过synchronized关键字,而是通过java.util.concurrent.locks.ReentrantLock来实现。
ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的。 其可重入性是基于Thread.currentThread()实现的:如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

1、公平锁

  • 公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
  • 锁的实现方式是基于如下几点:
    - 表结点Node和状态state的volatile关键字。
    - sum.misc.UnsafepareAndSet的原子操作(见附录)。

2、非公平锁

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

  • ReentrantLock锁都不会使得线程中断,除非开发者自己设置了中断位。
  • ReentrantLock获取锁里面有看似自旋的代码,但是它不是自旋锁。
  • ReentrantLock公平与非公平锁都是属于排它锁。

二、ReentrantLock的可重入性分析

ReentrantLock重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了锁, 那该线程下的所有方法都可以获得这个锁。ReentrantLock的锁依赖只有 NonfairSync和FairSync两个实现类, 他们的锁获取方式大同小异。

//可重入性的实现基于下面代码片段的 else if 语句
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {...// 尝试获取锁成功}else if (current == getExclusiveOwnerThread()) {// 是当前线程,直接获取到锁。实现可重入性。int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

此处有两个值需要关心:

  	 //持有该锁的当前线程private transient Thread exclusiveOwnerThread;-----------------两个值不在同一个类----------------/*** 同步状态* 0: 初始状态-无任何线程得到了锁* > 0: 被线程持有, 具体值表示被当前线程持有的执行次数* 这个字段在解锁的时候也需要用到。* 注意这个字段的修饰词: volatile*/private volatile int state;

三、ReentrantLock锁的实现分析

1、公平锁和非公平锁

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
  • tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。
  • addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值,而非公平锁在对于新晋线程有很大优势。
  • acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。
  • selfInterrupt 如果线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。
  • ReentrantLock 对线程的阻塞是基于 LockSupport.park(this); (AbstractQueuedSynchronizer#parkAndCheckInterrupt,先决条件是当前节点有限次尝试获取锁失败。)

公平锁和非公平锁在锁的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。但是当并发情况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可通过CPU对共享变量加锁的形式,实现数据变更的原子操作。volatile 和 CAS的结合是并发抢占的关键。

2、公平锁FairSync

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:

if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;
}

其中hasQueuedPredecessors是用于检查是否有等待队列的。

 public final boolean hasQueuedPredecessors() {Node t = tail; // Read fields in reverse initialization orderNode h = head;Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());}

3、非公平锁NonfairSync

非公平锁在实现的时候多次强调随机抢占:

if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}
}

与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁。如果被加入了等待队列后则跟公平锁没有区别。

4、ReentrantLock锁的释放

ReentrantLock锁的释放是逐级释放的,也就是说在可重入性场景中,必须要等到场景内所有的加锁的方法都释放了锁, 当前线程持有的锁才会被释放。
释放的方式很简单, state字段减一即可:

protected final boolean tryRelease(int releases) {//  releases = 1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;
}

5、ReentrantLock等待队列中元素的唤醒

当当前拥有锁的线程释放锁之后, 且非公平锁无线程抢占,就开始线程唤醒的流程。
通过tryRelease释放锁成功,调用LockSupport.unpark(s.thread); 终止线程阻塞。

private void unparkSuccessor(Node node) {// 强行回写将被唤醒线程的状态int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;// s为h的下一个Node, 一般情况下都是非Null的if (s == null || s.waitStatus > 0) {s = null;// 否则按照FIFO原则寻找最先入队列的并且没有被Cancel的Nodefor (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 再唤醒它if (s != null)LockSupport.unpark(s.thread);
}

6、ReentrantLock内存可见性分析

try {lock.lock();i ++;
} finally {lock.unlock();
}

可以发现哪怕在不使用 volatile关键字修饰元素i的时候, 这里的i 也是没有并发问题的。
volatile 是Java语言的关键字, 功能是保证被修饰的元素(共享变量):

  • 任何进程在读取的时候,都会清空本进程里面持有的共享变量的值,强制从主存里面获取;
  • 任何进程在写入完毕的时候,都会强制将共享变量的值写会主存。
  • volatile 会干预指令重排。
  • volatile 实现了JMM规范的 happen-before 原则。

更多推荐

十四、并发编程之ReentrantLock公平锁和非公平锁原理详解

本文发布于:2024-02-25 07:57:28,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1698297.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:公平   详解   原理   十四   ReentrantLock

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!