ReentrantLock源码解析和AQS常见问题分析

ReentrantLock和AQS常见问题分析

一、前言

本文利用ReentrantLock作为阅读AQS的切入口,通过问答的方式让大家更好的去理解今天要掌握的点,也欢迎大家说说自己的答案。

二、本文大纲

脑图是个很好的辅助记忆工具,也能提高自己的逻辑思维能力,下文我会通过这个脑图来讲解。
ReentrantLock源码解析和AQS常见问题分析_第1张图片

三、问答环节

  1. 什么是AQS
    AQS是抽象队列同步器,AQS内部维护了一个用volatile修饰的state变量和一个FIFO的双向队列,线程通过CAS修改state,如果CAS失败就将当前线程组装成Node添加到双向队列里面。很多同步工具都继承了AQS类。
  2. AQS能实现哪些同步工具
同步工具 同步工具与AQS的关联
ReentrantLock 使用AQS保存锁重复持有的次数。
Semaphore 使用AQS同步状态来保存信号量的当前计数。
CountDownLatch 使用AQS同步状态来表示计数。
ReentrantReadWriteLock 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。
ThreadPoolExecutor Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。
  1. 你能通过AQS实现一个同步锁吗?
    可以,只需要继承AQS,在重写tryAcquire和tryRelease方法去操作State节点就能实现一个简易的同步锁了。
public class TestMyAQS extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int arg) {
        while (true){
            if(compareAndSetState(0, arg)){
                return true;
            }
        }
    }

    @Override
    protected boolean tryRelease(int arg) {
        setState(0);
        return true;
    }
}
  1. AQS中为什么要有一个虚拟的head节点
    waitStatus维护了下个节点的状态,可能是挂起也可能是取消,而释放锁的时候需要挑选一个挂起的线程去释放锁,第一个挂起的线程没有前置节点,所以需要创建一个虚拟节点。
  2. AQS中为什么选择使用双向链表,而不是单向链表
    在ReentrantLock中,当调用中断线程排队方法lockInterruptibly时候,会将waitStatus设置成1,从AQS队列中移除,如果是单项链表需要从头开始遍历,很耗时间。
  3. 如果头结点的下一个节点取消了,唤醒节点的时候为什么从后往前找
    从代码可以看到插入的时候node的pred节点能保证一定不为空,如果从头向尾找可能出现调度问题,compareAndSetTail已经成功了,此时pred节点的next节点还是空的。
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;
    }
  1. ReentrantLock中公平锁和非公平锁有什么区别
  • 公平锁多了一个方法保证拿到的锁的节点都是第一个节点
  • 公平锁相对非公平锁性能要差
  • 公平锁不会出现锁饥饿的问题
  1. 公平锁一定公平吗
    不一定,假设A线程获取到了锁,B线程没获取到锁,正在添加到队列里的过程中,A线程释放锁了,C线程进来了,发现tail==head,于是后来的C获取到了锁。

步骤一、线程B添加队列
注释地方设置tail = head

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head; // B线程设置tail = head
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

步骤二、线程C尝试获取锁
hasQueuedPredecessors,由于h==t,返回false

public final boolean hasQueuedPredecessors() {
      Node t = tail;
      Node h = head;
      Node s;
      return h != t && // 由于h==t,返回false
          ((s = h.next) == null || s.thread != Thread.currentThread());
}

步骤三、线程A释放锁

步骤四、线程C尝试获取锁
CAS成功,线程C获取到了锁

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) { // 这里CAS成功,线程C获取到了锁
                    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;
        }
  1. ReentrantLock和Synchronized的区别
  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

你可能感兴趣的:(集合与并发,Java,java)