06_ReentrantLock——java并发系列(六)

188 阅读8分钟

ReentrantLock

一、概述

ReentrantLock意思为可重入锁,也就是如果一个线程成功获取了某个ReentrantLock锁,那么,他可以对该锁,重复加锁,而不会阻塞。这一点,与synchronized行为是一致的。

❄️ ReentrantLock vs synchronized

ReentrantLock是在JDK1.5之后提供的,1.5早期版本中,重⼊锁的性能远远好于synchronized,但从JDK6.0开始,JDK在synchronized上做了⼤量的优化,使得两者的性能差距并 不⼤。重⼊锁对逻辑控制的灵活性要远远好于synchronized。

对比项ReentrantLocksynchronized
性能1.5版本性能更好,之后已经相差不大--
灵活性获取锁时,支持超时、中断以及尝试获取锁不灵活
锁类型支持公平&非公平锁非公平锁
条件队列可以关联多个条件队列仅支持一个条件队列
使用上加锁后,执行代码应立即使用try块,在finally中使用unlock释放锁无需显示释放

synchronized做了哪些性能优

不可不说的Java“锁”事

结论:

  • synchronized使用起来更方便,如果需求不是很复杂,建议直接使用synchronized。这样如果后续synchronized在JVM层面继续优化,代码无需任何改动,只要升级JVM,即可享受性能提升。
  • ReentrantLock则支持的功能更加丰富,实现复杂需求的时候,则需要它
    • 支持公平锁
    • 获取锁时,支持超时、中断,。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。而ReentrantLock提供了可响应中断以及设置超时时间获取锁的方法,给破坏死锁条件,提供了解决方案。
    • 支持非阻塞地获取锁(tryLock)。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回。
    • 可以关联多个条件队列,实现更复杂的需求。

二、使用

2.1 公平 & 非公平

ReentrantLock的构造函数,提供一个boolean类型的可选参数,用于指定是否为公平锁,默认是非公平锁。

  • 公平锁:先进先出,一个线程获取锁,如果AQS的等待队列中存在其他线程在等待,则追加到等待队列中,等待锁。
  • 非公平锁:与公平相对,一个线程获取锁,即使等待队列中有其他线程在等待,它也可以尝试获取锁,获取不到,才进入等待队列中。

性能

相对来说,非公平锁性能更好,甚至是好很多,不能从代码逻辑的角度去考虑,而需要在操作系统层面考虑,线程切换是非常耗时的。非公平锁如果能够抢占到锁,就避免了阻塞再唤醒的开销了。

饥饿

非公平锁,有饥饿的问题,可能因为总有其他线程抢占锁,导致在等待队列中的线程,长时间无法获取到锁,也就是所谓的“饥饿”。相对的,公平锁可以避免这个问题。

==注意==

tryLock()并不会遵守公平锁的协议,使用该方法可以抢占锁。

2.2 灵活加锁

ReentrantLock与synchronized相比,很大的一个优势,就是加锁的方式更加灵活丰富,本小节介绍如何灵活的加锁。

加解锁的范式

加锁完成后,一般立即跟随一个try块,并在finally中解锁,以保证能正确的解锁。

private final ReentrantLock lock = new ReentrantLock();

public void m() {
 lock.lock();  // block until condition holds
 try {
   // ... method body
 } finally {
   lock.unlock()
 }
}

各种加锁方式

  • lock(),常规加锁,与synchronized类似;
  • lockInterruptibly(),加锁,可以响应中断,如果当前线程已经有中断标识或者在获取锁的过程中被中断,则会抛出中断异常;
  • tryLock(),尝试加锁,即使失败了,也不会阻塞,只是返回false。==该方法并不会遵守公平锁的协议,即使在公平模式下,该方法也会抢占锁。==
  • tryLock(long timeout, TimeUnit unit),在给定的时间内尝试获取锁
    • 可以响应中断,行为与lockInterruptibly()一致;
    • 遵守公平协议;
    • 如果timeout<=0,则不会等待;

tryLock vs 公平锁

已知tryLock()不遵守公平协议,tryLock(long timeout, TimeUnit unit)遵守公平协议,那么如果我希望tryLock()的时候遵循公平锁协议、,tryLock(long timeout, TimeUnit unit)不遵循公平锁协议该怎么办呢?

  • tryLock()遵循公平锁协议,直接使用tryLock(long timeout, TimeUnit unit)方法替代,timeout<=0即可;
  • tryLock(long timeout, TimeUnit unit)不遵循公平锁协议,则组合使用,这样lock.tryLock() || lock.tryLock(timeout, unit)即可。

2.3 条件队列

ReentrantLock可以使用newCondition()方法,创建一个条件队列,与synchronized配合Object类似。ReentrantLock的newCondition()方法,返回的Condition可以类比synchronized锁的那个Object的功能大致相同,方法类比:

  • Condition.await() --> Object.wait()
  • Condition.signal() --> Object.notify()
  • Condition.signalAll() --> Object.notifyAll()

区别:

  1. Condition提供了除可以响应中断的await方法外,还提供了,不响应中断的awaitUninterruptibly();
  2. 同一个ReentrantLock,可以创建多个Condition,也就是说ReentrantLock可以创建多个条件队列,而synchronized仅能有一个。

❄️ 多线程交替打印

一个常见的多线程面试题,多线程交替打印,即多个线程依次循环打印,考察多线程的通信机制。可以使用多种实现方式,包括有锁的,比如使用synchronized/ReentrantLock,无锁的,比如使用CAS。可以参考: 多线程交替打印的四种方法

本文主要使用有锁的方式实现,对比下synchronized/ReentrantLock实现区别:

synchronized 版本

使用synchronized + Object的wait + notifyAll组合,进行线程之间的同步。

==注意==

这里必须用notifyAll,notify仅唤醒wait等待队列中的一个线程,而被唤醒的这个线程不一定满足条件而执行,可能重新等待,导致wait等待队列中的线程,无法唤醒。

public class SynchronizedAlternatePrinter implements Runnable {

    private static int globalSeq = 0;

    private final int order;
    private final int totalThreadCnt;
    private final Object monitor;
    private final String printInfo;

    public SynchronizedAlternatePrinter(int order, int totalThreadCnt, Object monitor, String printInfo) {
        this.order = order;
        this.totalThreadCnt = totalThreadCnt;
        this.monitor = monitor;
        this.printInfo = printInfo;
    }

    @Override
    public void run() {
        synchronized (monitor) {
            while (true) {
                if (globalSeq % totalThreadCnt == order) {
                    System.out.println(printInfo);
                    globalSeq = (globalSeq + 1) % totalThreadCnt;
                    monitor.notifyAll();
                } else {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        }
    }
}

public class MultiThreadAlternatePrintTest {

    public static void main(String[] args) {
        testSynchronizedAlternatePrinter(5);
    }

    public static void testSynchronizedAlternatePrinter(final int total){
        final Object monitor = new Object();
        for (int i = 0; i < total; i++) {
            new Thread(new SynchronizedAlternatePrinter(i,total,monitor,String.valueOf(i))).start();
        }
    }
}

ReentrantLock 版本

使用ReentrantLock + Condition方式实现,充分利用了ReentrantLock可以创建多个ReentrantLock的特性,为每个线程分配了一个Condition,每个线程在执行完成之后,只要signal下一个线程对应的Condition即可,具体的实现,其实没太大差别。

==注意==

这里可以使用signal()替代,不过还是建议使用signalAll(),这是个好习惯。

public class ReentrantLockAlternatePrinter implements Runnable {
    private static int globalSeq = 0;

    private final int order;
    private final int totalThreadCnt;
    private final ReentrantLock lock;
    private final Condition curCondition;
    private final Condition nextCondition;
    private final String printInfo;


    public ReentrantLockAlternatePrinter(int order, int totalThreadCnt, ReentrantLock lock, Condition curCondition, Condition nextCondition, String printInfo) {
        this.order = order;
        this.totalThreadCnt = totalThreadCnt;
        this.lock = lock;
        this.curCondition = curCondition;
        this.nextCondition = nextCondition;
        this.printInfo = printInfo;
    }


    @Override
    public void run() {

        lock.lock();
        try {
            while (true) {
                if (globalSeq % totalThreadCnt == order) {
                    System.out.println(printInfo);
                    globalSeq = (globalSeq + 1) % totalThreadCnt;
                    nextCondition.signalAll();
                } else {
                    try {
                        curCondition.await();
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        } finally {
            lock.unlock();
        }
    }
}

public class MultiThreadAlternatePrintTest {

    public static void main(String[] args) {
        testReentrantLockAlternatePrinter(5);
    }

    public static void testReentrantLockAlternatePrinter(final int total){
        final ReentrantLock lock = new ReentrantLock();

        Condition begin = lock.newCondition();
        Condition cur = begin;
        for (int i = 0; i < total; i++) {
            Condition next = i == total -1 ? begin : lock.newCondition();
            new Thread(new ReentrantLockAlternatePrinter(i,total,lock,cur,next,String.valueOf(i))).start();
            cur = next;
        }
    }
}

三、实现原理

主要是基于AQS实现,不清楚的可以看下之前的AQS原理的文章:04_AQS框架——java并发系列(四)

公平锁 & 非公平锁tryAcquire对比

非公平锁:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // state是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;
}

公平锁:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //即使状态是0,也要判断队列中是否存在等待的线程
        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;
}

差别就是在判断c==0成立后,公平锁还要再判断是否有后继节点(hasQueuedPredecessors),非公平锁则不需要

hasQueuedPredecessors 是否存在下一个节点

hasQueuedPredecessors判断是否存在下一个节点,在公平锁获取锁之前,会判断是否存在下一个节点。这里只是判断是否存在,没有判断下一个是否取消。

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t && //队列不是空(head是哨兵,未初始化的时候,h和t都是null,也是相等的)
        ((s = h.next) == null || s.thread != Thread.currentThread());// 存在next且next不是当前线程
}
公平锁可以保证完全公平么

如果一个线程,已经进入了队列,那么从代码中可以得知,其他线程一定等到该线程获取锁或者因超时或者中断取消后才能获取锁。但是假如还没进入队列,此时而锁恰好释放,那么就可能会其他线程抢到。

unlock - 释放锁

无论是公平模式,还是非公平模式,释放锁实现的都是一样的逻辑:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 这个操作一定要在setState之前,因为在这个之前是不会并发的,之后是会的。
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

没什么好说的,就是把state - 1,如果-1后能等于0,则设置独占线程为null,注意这个在setState之前。

因为释放锁,一定获取了锁,所以这里是单线程的,不会并发,用setState就可以了,不需要CAS。在setState之前,都不会并发,因此,一定要在这之前,setExclusiveOwnerThread(null)。

Condition

额,就是调用的AQS的newCondition,不多说了。