并发包(JUC)之Lock和AQS
1、Lock接口的实现——并发包锁
(1)ReentrantLock
重入锁,重入锁指线程在获得锁之后,当该线程再次请求获得该锁不需要阻塞,而是可以直接获得锁,同时计数器增加重入次数。不同线程还是会阻塞的。
(2)ReentrantReadWriteLock
重入读写锁,实现了ReadWriteLock接口,它维护两个锁,一个ReadLock,一个WriteLock,这两个都实现了Lock。基本原则是:读和读不互斥,读和写互斥,写和写互斥。适合读多写少的场景。
(3)StampedLock
jdk1.8引入的新的锁机制,可以认为是对ReentrantReadWriteLock的改进版本。解决大量的读线程存在,可能会引起写线程的饥饿的问题。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。
2、ReentrantLock的实现原理
重入锁的设计目的:比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
关系图:
(1)AQS
全称 AbstractQueuedSynchronizer,是实现Lock的核心组件。从功能层面分为两种:独占和共享。
* 独占锁,ReentrantLock就是以独占方式实现的互斥锁。
* 共享锁,如ReentrantReadWriteLock。
AQS内部实现:AQS队列内部维护了一个FIFO的双向链表,双向链表可以从任意一个节点访问很方便地访问前驱和后继节点(非公平锁很好地利用了这一点特性),每个节点(Node)由线程和等待状态封装而成,当线程争抢锁失败,会被封装成Node添加到链表末端。
Node节点:
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; //前驱节点 volatile Node next; //后继节点 volatile Thread thread;//当前线程 Node nextWaiter; //存储在condition队列中的后继节点 //是否为共享锁 final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } //将线程构造成一个Node,添加到等待队列 Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } //这个方法会在Condition队列使用,后续单独写一篇文章分析condition Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
(2)结合AQS看ReentrantLock实现原理
假设ThreadA、B、C三个线程同时访问一个同步块:
如果ThreadA通过CAS获得了锁,此时state=1,exclusiveOwnerThread=ThreadA。ThreadB和ThreadC会被封装成Node节点,形成双向链表。
(3)公平锁和非公平锁(默认)
FairSync在尝试获取锁时,多了一个hasQueuePredecessors判断,也就是如果当前线程(Node)有前置节点,则不会进行CAS去竞争锁,NonFairSync则不会做这个检查,直接通过CAS去竞争锁。
(4)ReentrantLock偏向锁的巧妙实现
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
nonfairTryAcquire方法是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果c !=0 说明有线程正拥有了该锁,但如果发现是自己拥有该锁的话,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能。
(5)ReentrantLock和synchronized区别
• ReentrantLock 会通过多次CAS尝试获取锁,这降低了线程上下文切换的概率,一定程度提高了性能;而 synchronized 一旦升级为重量级锁,只要申请锁失败就会就会进入队列中阻塞。
• ReentrantLock 功能更多,比如对于读多写少的场景,可以使用读写锁,性能要高很多。
• ReentrantLock 可以被中断,但是 synchronized 不能被中断。
• ReentrantLock 可以通过 tryLock(timeout) 来设置线程等待时长。
• ReentrantLock 有公平锁和非公平锁,synchronized 只有非公平锁。
3、Synchronized与ReentrantLock实现原理有何不同?
锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,以此来保证每个线程都能拥有对该int变量的可见性和原子修改,其本质是基于AQS框架。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {
private volatile int state;
}
推荐文章:
分析ReentrantLock原理:https://mp.weixin.qq.com/s/3tqBo47GtG3ljdrig2b8AA