Java Lock锁介绍(可重入锁,独占锁,共享锁,锁降级)AbstractQueuedSynchronizer AQS源码解读

一、可重入锁

    可重入锁是指同一个线程连续调用lock.lock()不会被阻塞。重入锁的一种实现方式是为锁关联一个计数器和拥有线程。当计数器为0时,表示锁没有被任何线程占有。初次获取锁时,锁时计数器加1,并且拥有线程设置为当前线程。释放锁时计数器减1,当计数器为0时释放锁并清空关联线程。java 的synchronized 和ReentrantLock都是可重入锁。

二、独占锁,共享锁

  1. 独占锁

    独占锁是指同一时间锁只能被一个线程占有,任何竞争锁的线程都会失败,直至持有锁的线程释放资源,独占锁也被称为排他锁。synchronized是独占锁。

  1. 共享锁

    共享锁是指同一时间可以被多个线程占有,假设锁最多允许n个线程共享,当线程竞争锁如果持有锁的线程数小于n,则线程可以获取共享锁,当持有锁的线程数为n时,获取锁的线程需要排队,直至持有锁的线程释放资源。

三、锁降级

    这里的锁降级并不是指synchronized的锁降级,synchronized只能按照偏向锁–>轻量级锁–>重量级锁升级。事实上锁降级和synchronized的锁升级也不是一个维度的。

    读写锁:读锁与读锁之间共享,读写之间排他。

    锁降级是指写锁降级成读锁。如果当前线程获取到了写锁,随后释放了写锁,然后获取到读锁,这个过程不是锁降级。锁降级是指线程拥有写锁,获取读锁,释放写锁的过程。

四、J.U.C源码分析

    Lock接口是1.5开始出现的,由Java并发大师Doug Lea撰写,主要记录下AbstractQueuedSynchronizer,ReentrantLock,ReentrantReadWriteLock的源码学习记录。

1. AbstractQueuedSynchronizer

    几乎所有Lock下锁的实现都聚合了一个AbstractQueuedSynchronizer(以下称AQS)实现自己特定的功能,Doug Lea也希望AQS能应用于大部分自定义锁的场景,通常只需要定义一个静态内部类Sync继承AQS,根据具体的需求重写相应的模板方法,如tryRelease(释放锁),tryAcquire(获取锁)等方法就够了。AQS没有实现任何Lock相关的接口,仅提供了共享锁,独占锁获取的模板方法,内部持有一个类似CLH的队列,用来存放等待获取锁的线程(当然如果锁竞争不激烈,就没有这个队列),维护线程的状态,及

    当线程获取不到锁时,会创造一个Node节点,从尾部进入AQS的同步队列,独占锁和共享锁共用一个队列。当入队时,为了保证线程安全,采用自旋+CAS更新尾节点的方式。出队时,直接简单地更新头节点就行了,因为出队的Node(线程)必然是持有锁的线程。AQS内部的Node类主要的实例域:

		// 当前节点的状态
        volatile int waitStatus;
        
		// 当前node的前驱节点
        volatile Node prex;


		// 当前node的后继节点
        volatile Node next;

        // 当前节点线程对象
        volatile Thread thread;

		// 表明当前节点模式,或用于Condition
        Node nextWaiter;

Node状态 waitStatus说明:

状态说明
CANCELLED1由于中断或者等待超时,线程取消了对锁的争夺,马上要脱离等待队列了
SIGNAL-1后继节点的线程处于等待状态,前面的线程释唤醒它
CONDITION-2节点在等待队列中,线程等待在Condition上,当Condition#signal()或signalAll()会将节点从等待队列转移到同步队列中
PROPAGATE-3下一次的共享状态会无条件的传播下去。

Node在AQS同步队列结构

    AQS内部存储了两个节点:头节点、尾节点,Node两个指针(prev、next)共同构成了一个队列。头尾节点默认延迟初始化,当第一次有节点入队时进行初始化。获取锁的方法:

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    tryAcquire(arg)需要由子类重写,且tryAcquire方法要求是非阻塞的、线程安全的、实现尽量简单。当子类获取不到锁时会入队。方法如下:

addWaiter(Node.EXCLUSIVE):构造Node节点并加入等待队列

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试快速插入,如果tail尾节点不为空直接尝试CAS从尾部插入新的节点
        // 如果失败说明 尾节点还没初始化或有其他线程参与竞争需要自旋插入直至成功 
        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) {
            	// 初始化头尾节点,CAS失败了也没关系,
            	// 其他线程已经初始化好了
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    acquireQueued(addWaiter(Node.EXCLUSIVE), arg),加入成功后会自旋获取锁,并不会一直自旋消耗CPU,如果满足条件shouldParkAfterFailedAcquire(p, node),会调用LockSuport#park();使当前线程进入等待状态。当线程被唤醒时,有可能是头节点释放锁,也有可能是前驱节点因为中断或者取消离开队列。当线程被唤醒时,如果前驱节点是头节点,则会开始竞争锁;否则检查,前驱节点prex是否被取消了,prex会跳过取消的节点 “挂在”最近的一个未取消的节点上面,以便前面的兄弟节点释放锁时能够成功的唤醒它。

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	// 获取前驱节点,只有当前node的前驱节点是头节点时才会参与锁的竞争,FIFO
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire 从方法名可以看出来,
                // 是否需要在此次获取锁失败的时候park当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

   shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 如果当前节点的前驱节点已经被设置为SIGNAL(-1)时,可以安心的park了,
            // 前面的线程会在合适的时候唤醒它。
            return true;
        if (ws > 0) {
          	// 状态大于0,说明当前的线程是CANCELLED (1)前驱节点就要离队了
          	// 这个时候就不能安心的park了,需要做点事情,前驱节点指向第一个没有被取消的node
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 说明是 0 或 -3(前节点没有初始化或者其他情况),需要设置为-1
            // 使得前节点能够叫醒自己
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 会进行acquireQueued 方法中的自旋,如果前驱节点已经是头节点了,
        // 就可以争夺锁了,否则下一次进入此方法就有能被park()而进入等待了{
        return false;
    }

    释放锁:

   public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
  private void unparkSuccessor(Node node) {
    
    	// 
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
            
      // 后继节点不为空则唤醒后继节点
      // 否则会从tail尾部找一个没有被取消的node(线程)进行唤醒      
        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);
    }

AQS获取锁的流程总结:

总结:

  1. 获取锁失败会构造一个node节点插入至同步队列尾部
  2. 如果前驱节点是头节点则会参与锁的竞争,如果不是则会将其前驱节点状态改为-1,保证前驱节点释放锁或者取消时能够唤醒它。(其实这种说法并不严谨,当释放锁时后继节点如果被取消了,则会从tail尾部遍历唤醒一个非取消状态的节点(1),不过能保证前驱节点设置为-1,总有线程能够唤醒它。
  3. 释放锁时有需要则会唤醒一个线程,如果后继节点没有取消,则是后继节点,否则会从尾部找一个非取消状态的线程,来唤醒它。这并不违反FIFO,因为唤醒的线程会判断前驱节点是否为头节点,如果不是则会继续睡眠,这次唤醒只是通知它前面有节点取消了,prev->prev这个链需要跳过这些取消的节点。
  4. 共享锁和排他锁流程相似,这里就不分析了
2. ReentrantLock

    可重入锁式独占锁,内部聚合了一个自定义的Sync继承了AQS实现了自己的功能。默认是非公平锁,公平锁是严格的FIFO,先获取锁的线程一定开获取到锁,这样会损失一些行能,因为获取锁的时候要判断是否有更早的线程获取锁,有的话没有竞争资格,在后面排队。非公平锁则不管这些,线程来了直接参与竞争,一定程度上避免了线程的切换,性能更好。

    分析下可重入的实现原理:

   // 非公平锁获取锁
   final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 获取锁的线程 是否和当前线程相等 是的话就将AQS的status +1
            // 释放锁的时候也不直接释放,判断 status 为 1 时才进行释放
            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;
        }
        
         protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	// 非公平锁会判断有没有 ‘前辈’在等着,有的话!hasQueuedPredecessors()为
            	// false ,直接丧失竞争资格,去AQS的同步队列后面排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

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


3. ReentrantReadWriteLock

待更新(2020-08-08 17:41:00)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值