Java-并发-锁-ReentrantLock

Java-并发-锁-ReentrantLock

摘要

ReentrantLock是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)系列的可重入锁。本文会分析他的lock, unlock等重要方法,还涉及公平/非公平概念对比,及对比synchronized。

关于Condition的分析,请参见本文姊妹篇:Java-并发-Condition

0x01 基本概念

ReentrantLock是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)系列的可重入锁。它相对于LockSupport来说属于是高层API,被鼓励在用户开发中使用。

ReentrantLock基本特点如下:

  • 等待可中断
    获取锁时可以指定一个超时时间,如果超过这个时间还没有拿到锁就放弃等待
  • 公平性
    公平锁就是按线程申请锁时候FIFO的方式获取锁;而非公平锁没有这个规则,所有线程共同竞争,没有先来后到一说
  • 绑定对象
    一个synchronized绑定一个Object用来wait, notify等操作;而ReentrantLock可以newCondition多次等到多个Condition实例,执行await, signal等方法。

0x02 实现原理

限于篇幅,这里可以大概说下其原理。

2.1 AQS

AQS全称AbstractQueuedSynchronizer,他是ReentrantLock内部类NonfairSyncFairSync的父类Sync的父类,其核心组件如下:

  1. state,int 类型,用来存储许可数
  2. Node双向链表,存储等待锁的线程

Node就是AQS的内部类,这里可以简单看看Node定义:

static final class Node {
    // 表明等待的节点处于共享锁模式,如Semaphore:addWaiter(Node.SHARED)
    static final Node SHARED = new Node();
    // 表明等待的节点处于排他锁模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
    static final Node EXCLUSIVE = null;

    // 线程已撤销状态
    static final int CANCELLED =  1;
    // 后继节点需要unpark
    static final int SIGNAL    = -1;
    // 线程wait在condition上
    static final int CONDITION = -2;

    // 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 
    static final int PROPAGATE = -3;
    
    // 这个waitStatus就是存放以上int状态的变量,默认为0
    // 用volatile修饰保证多线程时的可见性和顺序性
    volatile int waitStatus;
    
    // 指向前一个Node的指针
    volatile Node prev;
    
    // 指向后一个Node的指针
    volatile Node next;
 
    // 指向等待的线程
    volatile Thread thread;

    // condition_queue中使用,指向下一个conditionNode的指针
    Node nextWaiter;

    // 判断是否共享锁模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 返回前驱结点,当前驱结点为null时抛出NullPointerException
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    // 用来初始化wait队列的构造方法;也被用来做共享锁模式
    Node() {
    }

    // 在addWaiter方法时,将指定Thread以指定模式放置
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // Condition使用的构造方法
    Node(Thread thread, int waitStatus) { 
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}    

AQS的Node等待队列双向链表如下图:
Java-并发-锁-ReentrantLock_第1张图片

2.2 非公平锁的实现

默认采用非公平的实现NonFairSync

2.2.1 lock()

lock()方法流程如下图:

Java-并发-锁-ReentrantLock_第2张图片

可以看到,lock()方法最核心的部分就是可重入获取许可(state),以及拿不到许可时放入一个AQS实现的双向链表中,调用LockSupport.park(this)将自己阻塞。就算阻塞过程被中断唤醒,还是需要去拿锁,直到拿到为止,注意,此时在拿到锁之后还会调用selfInterrupt()方法对自己发起中断请求。

2.2.2 unlock()

Java-并发-锁-ReentrantLock_第3张图片

2.3 公平锁的实现

他的实现和非公平锁有少许区别:

 static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
    // 这里不再有非公平锁的
    // if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());
    // 也就是说,公平锁中,必须按规矩办事,不能抢占
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        // 这里多了一个 !hasQueuedPredecessors(),也就是不需要考虑wait链表
        // 否则就老实按流程走acquireQueued方法
            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;
    }
}

下面看看的hasQueuedPredecessors实现

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    // h != t 代表wait链表不为空状态
    // (s = h.next) == null代表wait链表已经初始化
    // s.thread != Thread.currentThread()代表当前线程不是第一个在wait链表排队的线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

0x03 ReentrantLock和synchronized对比

ReentrantLock和synchronized对比如下:

可重入 等待可中断 公平性 绑定对象数 性能优化
synchronized 支持 不支持 非公平 只能1个 较多
ReentrantLock 支持 支持 非公平/公平 可以多个 -

0x04 公平锁与非公平锁

非公平锁比起公平锁来说,唯一区别就是非公平锁可以快速用compareAndSetState(0, acquires)进行抢占,而公平锁必须老老实实FIFO形式排队;但unlock唤醒的时候是没有区别的。

0x05 总结

  • state采用volatile,保证有序性和可见性
  • 大量使用如unsafe.compareAndSwapInt(this, stateOffset, expect, update);此类的CAS操作,保证原子性,同时在竞争小的时候效率胜过synchronized
  • 所谓的加锁就是AQS.state++。且该锁是可重入的,每次就state加1,unlock一次减一。两个操作必须一一对应,否则其他等待锁的线程永远等待。
  • 所谓的等待锁阻塞,就是放在一个链表里,然后用LockSupport.park(this)阻塞
  • 就算用中断唤醒已经等待锁而阻塞的线程,依然必须直到获取锁才能执行。且在其后如果执行可中断操作,会发生中断!

关于Condition的分析,请参见本文姊妹篇:Java-并发-Condition

你可能感兴趣的:(源码,java,并发)