Lock及AQS底层分析

Lock基本介绍

Lock接口是除了synchronized另一种加锁方式,它拥有比synchronized更加灵活的加锁方式,是JUC中的核心组件
Lock本质上是一个接口,它定义了释放锁与获得锁的抽象方法。实现Lock接口的类主要有以下几个实现
ReentrantLock:可重入锁,是唯一实现Lock接口的类(其他都是在类里面的小类),重入锁是指的在线程获得锁之后,再次获得该锁不需要再阻塞,而是直接关联一次计数器,增加重入次数。
ReentrantReadWriteLock :可重入读写锁,实现了ReadwWriteLock接口,在这个类中维护了两个锁, 分别是ReadLock与WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少场景下解决线程安全问题的工具,基本原则是:读与读不互斥、读写互斥、写写互斥。
StampedLock:StampedLock是JDK1.8之后引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读写功能使得读读之间安全,但是读写冲突,如果存在大量的读操作存在,可能会引起写线程饥饿(如果一个线程因为 CPU 时间全部被其他线程抢走而得不到 CPU 运行时间,这种状态被称之为“饥饿”)。StampedLock是一种乐观的读策略,使得乐观锁不会完全阻塞写操作。

Lock类关系图

在这里插入图片描述
主要API:
Lock.lock():如果锁可用则获取锁,不可用则阻塞等待锁释放后再获取。
Lock.lockInterruptibly():与lock方法类似,但是阻塞的线程可以中断,抛出中断异常InterruptedException.
Lock.tryLock(): 尝试获得锁,如果成功则返回true,失败不阻塞。
Lock.tryLock(long time, TimeUnit unit):带有超时时间获取锁方法
Lock.unLock():释放锁。

ReentrantLock 重入锁

ReentrantLock与synchronized都是可重入锁,可重入锁就是线程T1获得锁后 再次通过lock方法去获得锁可以成功获得锁,增加重试次数。举个实例。

	public class ReentrantLockDemo {
    private static  int count=0;
    static Lock lock=new ReentrantLock();
    private static void inc(){
        lock.lock();
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        count++;
        lock.unlock();
    }

    public static void main(String[] args)throws InterruptedException {
        for(int i =0;i<1000;i++){
            new Thread(()->{
                ReentrantLockDemo.inc();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println("result:"+count);
    }
}

打印结果是result>1。即重入线程多次。

ReentrantReadWriteLock 可重入读写锁

无论是ReentrantLock还是synchronized都是属于排他锁,即在同一时刻只有一个线程能够访问锁,而ReentrantReadWriteLock 可以在同一时刻运行多个线程进行读操作,所以在读多写少的情景下,
ReentrantReadWriteLock 能够提供比ReentrantLock与synchronized更好的并发性和吞吐量。
举个例子:

public class ReentrantReadWiteLockDemo {
    static Map<String,Object> cacheMap=new HashMap<>();
    static ReentrantReadWriteLock rrw=new ReentrantReadWriteLock();
    static Lock rL=rrw.readLock();
    static Lock wL=rrw.writeLock();
    public static final Object get(String key){
        System.out.println("开始读数据");
        rL.lock();

        try {
      	    return  cacheMap.get(key);
        }catch (Exception e) {

            e.printStackTrace();
            return null;
        }finally {
            rL.unlock();
        }
    }
    public static  final void put(String key,Object value){
        System.out.println("开始写数据");
        wL.lock();
        try{
            cacheMap.put(key,value);
        }finally {
            wL.unlock();
        }
    }
    
}

在这个案例中,通过hashMap 来模拟一个内存缓存,然后使用读写锁来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读写锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。 在执行写操纵的时候,线程必须获得写锁,如果已经有线程获取写锁/读锁,当前线程会被阻塞。

Lock实现原理

在Lock中,用到了一个同步队列AQS(AbstractQueuedSynchronizer),AQS是一个同步工具也是Lock用来实现线程同步的核心组件,如果要了解Lock锁机制与原理,那么AQS是绕不开的拦路虎。从使用层面来说AQS的功能非为两种:独占与共享。
独占锁是每次只有一个线程可以持有(ReentrantLock),共享锁是允许多个线程同时获取访问共享资源(ReentrantReadWriteLock)。

AQS内部实现

AQS队列内部维护的是一个FIFO(先进先出)的双向链表,双向链表的数据结构特点就在这里不一一赘述了,属于基本的数据结构。主要介绍链表节点的封装与操作。在AQS中每个节点都被封装成Node,而每个Node其实是由线程封装成的,这样也就可以理解为AQS就是根据场景需要将每个线程进行出对入队操作,当线程争抢锁失败后会被封装成Node加入到AQS队列中,当获取锁的线程释放锁以后,会从AQS队列中唤醒一个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 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() {    
    }
   

    Node(Thread thread, Node mode) {   //构造一个Node节点,添加到Condition队列
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

在Node中封装了不同的状态量来用于不同的状态,可以参考Node常量含义去了解,在源码中可以去清晰看出Node分装主要有前置节点、后置节点与当前Node节点的封装线程与状态量。 以下我将以ReentrantLock为切入点看这段运行过程。

ReentrantLock

public void lock() {
    sync.lock();
}
//sync
abstract void lock();

可以看到在ReentrantLock中 Sync中是抽有象方法,其有两个实现分别是NonfairSync与FairSync.
(非公平与公平)

lock方法
//NonfairSync
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

//AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();	//中断当前线程
}
//NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
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;
}
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
  
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

两者区别在于非公平在执行acquire方法之前就进行了cas去修改State为1,state默认情况下为0(单线程或者首次运行),所以肯定是会CAS成功,运行setExclusiveOwnerThread()方法,使得当前线程获得锁。
当有其他线程(第二、第三个线程)进入lock(),方法肯定会cas失败调用acquire(1),而在acquire(1)中首先通过tryAcquire()调用nonfairTryAcquire()方法,会再次进行CAS判断(防止在调用前面方法的途中上一个线程已经释放锁),CAS失败会进行下一步逻辑判断当前线程与获得锁线程是否一致,如果一致,则增加重入次数,更改State,返回true(本次操作终了).
否则返回flase.调用addWaiter(),将当前线程封装成Node节点加入AQS队列。首次运行因为pred==null则通过enq去封装Node.
enq中采用了for循环进行自旋操作。
enq里方法也很简单 首先判断尾节点是否为空(是否已经创建了AQS队列),尾节点为空,则新建一个Node节点,作为头节点,令头节点等于尾节点。当尾节点不为空(AQS队列种有线程/已经创建)则将本次循环的Node节点加入队列尾部。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();	//node节点的上一个节点(原队列尾节点)
            if (p == head && tryAcquire(arg)) {		//判断是否是头节点,是头节点才能tryAcquire
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //有可能tryAcquire失败(上一线程未释放锁)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
   
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;	//获取尾节点的waitStatus状态。
        if (ws == Node.SIGNAL)
            
            return true;
        if (ws > 0) {//跳过被取消的节点进行向头节点遍历。
            
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
         
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);	//更改
        }
        return false;
    }
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);	//挂起当前线程并设置状态为WATING
    return Thread.interrupted();	//返回当前线程是否触发过中断请求
}
 

acquireQueued的目的就是检测到AQS队列中链表节点是否正常,主要想要得到的是线程阻塞的信号。经过对节点线程状态的判断,设置本节点线程阻塞(已经置于AQS队列中)

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

selfInterrupt:标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求。

总结:
调用顺序:

ReentrantLock.lock()->Sync.lock()->NonfairSync.lock()->
AbstractQueuedSynchronizer.acquire(1)->
NonfairSync.tryAcquire(1)->NonfairSync.nonfairTryAcquire(1)->
AbstractQueuedSynchronizer.addWaiter(Node node)->
AbstractQueuedSynchronizer.acquireQueued(Node node,int arg)

主要方法逻辑:

1.首先通过CAS获得锁,CAS失败则调用acquire(1)走竞争逻辑
2.通过tryAcquire尝试获取独占锁,成功返回true.
3.如果tryAcquire失败,则通过addWaiter将当前线程封装成Node添加到AQS队列尾部
4.acquireQueued,将Node作为参数通过自旋取尝试获取锁。

unlock方法
public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;		//目测是用于后续更改锁状态。
    if (Thread.currentThread() != getExclusiveOwnerThread())	//非独占则报异常,这也是为什么unlock为报错,在没有加锁的情况下。
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {		//状态为0,则释放锁。
        free = true;
        setExclusiveOwnerThread(null);	//设置独占锁的线程为null
    }
    setState(c);	//更改state状态
    return free;
}
private void unparkSuccessor(Node node) { 
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

tryRelease 是个设置锁状态的操作,结果状态为0,就将排他锁的Owner设置为null.而unparkSuccessor就是一个唤醒后续节点的操作,值得注意的是释放锁操作唤醒后续节点是从尾节点开始扫描。这是因为enq方法中的创建、添加节点到AQS队列中是先将新节点的prev指向tail,再通过CAS将node更新掉原来的tail,最后才将tail节点的next指向node.
在 cas 操作之后, t.next=node 操作之前。 存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。 所以从后往前遍历,一定不会存在这个问题 。

总结:
调用顺序:

ReentrantLock.unlock()->AbstractQueuedSynchronizer.release(1)->
ReentrantLock.tryRelease(1)->AbstractQueuedSynchronizer.unparkSuccessor(Node node)->
AbstractQueuedSynchronizer.acquireQueued(Node node,int arg)

主要方法逻辑:

1.通过tryrelease进行更改独占线程(释放锁)
2.如果头节点不为空,且状态不为0,调用unparkSuccessor唤醒后续节点

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值