前言
保证线程安全的方式有很多,比如CAS操作、synchronized、原子类、volatile保证可见性和ReentrantLock等,这篇文章我们主要探讨ReentrantLock的相关内容。本文基于JDK1.8讲述ReentrantLock.
一、可重入锁
所谓可重入锁,即一个线程已经获得了某个锁,当这个线程要再次获取这个锁时,依然可以获取成功,不会发生死锁的情况。synchronized就是一个可重入锁,除此之外,JDK提供的ReentrantLock也是一种可重入锁。
二、ReentrantLock
2.1 ReentrantLock的简单使用
public class TestReentrantLock { private static int i = 0; public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); try { lock.lock(); i++; } finally { lock.unlock(); } System.out.println(i); } }
上面是ReentrantLock的一个简单使用案列,进入同步代码块之前,需要调用lock()方法进行加锁,执行完同步代码块之后,为了防止异常发生时造成死锁,需要在finally块中调用unlock()方法进行解锁。
2.2 ReentrantLock UML图
2.3 lock()方法调用链
上图描述了ReentrantLock.lock()加锁的方法调用过程。在ReentrantLock中有一个成员变量private final Sync sync,Sync是AQS的一个子类。ReentrantLock的lock()方法中,调用了sync的lock()方法,这个方法为抽象方法,具体调用的是NonfairSync中实现的lock()方法:
/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
在这个方法中,先尝试通过CAS操作进行加锁。如果加锁失败,会调用AQS的acquire()方法:
/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
在AQS的acquire方法中,先尝试调用tryAcquire方法进行加锁,如果失败,会调用acquireQueued进入等待队列当中。acquireQueued方法将会在第三章中讲解,先来看tryAcquire方法的内容。AQS的tryAcquire方法是一个模板方法,其具体实现在NonfairSync的tryAcquire方法中,里面仅仅是调用了nonfairTryAcquire方法:
/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ 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; }
在这个方法中,先获取state判断其是否为0。如果为0表示没有其他线程占用锁,会尝试通过CAS操作将state设为1进行加锁;如果state不为0,表示某个线程已经占用了锁,判断占用锁的线程是否为当前线程,如果是,则将state进行加1的操作,这就是ReentrantLock可重入的实现原理。
三、AQS
AQS即AbstractQueuedSynchronizer。AQS提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。AQS其实是CLH(Craig,Landin,Hagersten)锁的一个变种,下面来讲解AQS的核心思想及其具体实现。
3.1 state
/** * The synchronization state. */ private volatile int state;
state是AQS中最核心的成员变量。这是一个volatile变量,当其为0时,表示没有任何线程占用锁。线程通过CAS将state从0置为1进行加锁,当线程持有锁的情况下,再次进行加锁,会将state加1,即重入。
3.2 exclusiveOwnerThread
/** * The current owner of exclusive mode synchronization. */ private transient Thread exclusiveOwnerThread;
exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中的成员变量,其作用是实现可重入机制时,用于判断持有锁的线程是否为当前线程。
3.3 AQS等待队列
除了以上state和exclusiveOwnerThread两个重要的成员变量以外,AQS还维护了一个等待队列。当线程尝试加锁失败时,会进入这个等待队列中,这也是整个AQS中最核心的内容。这个等待队列是一个双向链表,其节点Node对等待加锁的线程进行封装。
/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; // 通过CAS操作将自身追加到链表尾部 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
当线程尝试加锁失败时,通过CAS操作将自身追加到链表尾部。入队之后,会调用acquireQueued在队列中尝试加锁:
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
在这个方法中,会判断其前置节点是否为头节点,如果是,则尝试进行加锁。如果加锁失败,则调用LockSupport.park方法进入阻塞状态,等待其前置节点释放锁之后将其唤醒。
3.4 AQS中的模板方法设计模式
AQS完美地运用了模板方法设计模式,其中定义了一系列的模板方法。比如以下方法:
// 互斥模式下使用:尝试获取锁 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } // 互斥模式下使用:尝试释放锁 protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } // 共享模式下使用:尝试获取锁 protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); } // 共享模式下使用:尝试释放锁 protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
这些方法在AQS中只抛出了UnsupportedOperationException异常,所以需要子类去实现它们。之所以没有将这些方法设计成为抽象方法,是因为AQS的子类可能只需要实现其中的某些方法即可实现其功能。
总结
不同版本的JDK,AQS的实现可能会有细微的差异,但其核心思想是不会变的,即线程加锁失败后,通过CAS进行入队的操作,并通过CAS的方法设置state来获得锁。
到此这篇关于Java多线程之深入理解ReentrantLock的文章就介绍到这了,更多相关Java ReentrantLock总结内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!
- 本文固定链接: https://zxbcw.cn/post/210659/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)