ReentrantLock重入锁
上次博客讲到了通过wait()方法和notify()方法来实现循环打印数字和字母得问题。其实使用重入锁也可以实现同样得功能,那么开始我们先通过源码来了解一下重入锁把。
public void lock() { sync.lock(); }
首先它有一个lock()方法,它用来加锁,从代码中可以看到,它调用得是sync.lock()方法,
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
在这个类里面,有一个静态抽象类Sync对象以及Sync得属性,因此我们可以知道它调用得是Sync里面得lock()方法,而Sync又是一个抽象类,lock()方法也是一个抽象方法,具体由它得子类去实现。
然后我们接着看ReentrantLock得构造方法
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
通过代码可以看到,在初始化得时候会初始化Sync对象,通过代码可以看出如果不带参数得话默认使用得是NonfairSync这个子类,也可以指定使用FairSync这个子类。好了,通过以上代码我们可以知道,ReentrantLock这个类会在实例化得时候指定FairSync或者NonFairSync,下面我们来介绍一下这两个类。首先通过字面意思可以看出前者得意思是“公平锁”,后者得意思是“非公平锁”。那么为什么要这么叫呢,其实是因为他们得lock()实现方法得差异。下面我们就来一一介绍,首先介绍FairSync
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); }
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
从代码可以看出最后得实现是acquire(1)这个方法,那么这个方法干了什么呢?看代码其实挺少得,其实干得事情并不少。首先会执行tryAcquire(arg)这个方法。同样,看代码。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 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,这个c很重要,它得含义就是这个“重入”得含义,等会再说。getState()方法通过代码可以看出返回得是ReentrantLock里面得state属性,因为是int类型,所以默认为0,代表没有线程正在使用它,这里讲了c这个变量得含义,我们接着往下看,如果c等于0,也就是说没有线程正在使用,那么他会进入下一个if判断,首先会执行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; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
这个方法是AbstractQueuedSynchronizer这个类里面得方法,通过代码可以看出这个类里面维护了一个FIFO队列,队列中每一个元素都是一个Node节点,其中Node对象有prev属性用来表示前一个节点,next属性用来表示下一个节点,thread属性用来标识当前线程,队列有一个head(头)节点和tail(尾)节点,head节点仅保存下一个节点得引用。就简单介绍这么点,因为这些是用来帮助我们理解上面代码得含义得。下面我用中文描述一下这个判断干了什么事情,返回 头节点 != 尾节点并且(头节点得下一个节点为空或者头节点得下一个节点得线程不等于当前线程),这么说有一点绕,其实我们看方法名可以知道这个方法是用来判断是否存在等待着得对象想要获得锁,首先假设队列为空,那么头节点等于尾节点,返回false,如果头节点不等于尾节点,那么头节点得下一个节点肯定不为空,然后判断头节点得下一个节点得线程是不是当前线程,如果是,返回false,如果不是,返回true。
然后我们再回归tryAcquire()方法。如果没有等着着得线程。那么它会执行compareAndSetState()方法。这个方法得底层是通过CAS来实现得,这里简单得介绍一下CAS,CAS是一种使用无锁得方式来实现线程安全得方法,这个方法有三个参数,一个是要更新得遍历V,一个是预期值E,一个是新值N,如果V == E,那么更新V为N,如果V != E,那么证明有其他线程更改了这个变量,这个方法不会做任何事情,你可以再重新执行这个方法或者选择放弃。主要流程就是这个,有兴趣得可以去了解一下CAS。如果操作成功,设置当前线程为正在使用得线程,返回true。这里讲解得是c等于0得情况,如果c不等于0呢?判断获得锁得线程是不是当前线程,如果是,c加1,看到这里其实也就明白了重入锁这个词是怎么来的了,它可以一直调用lock方法来加锁,每调用一次,state加1。
然后再回归acquire()方法。如果tryAcquire()方法获取锁成功,那么不会执行其他操作,如果失败,会执行acquireQueued(addWaiter(Node.EXCLUSIVE), args)这个方法,并且当前线程阻塞。那么这个方法又干了什么呢?先来看addWaiter(Node.EXCLUSIVE)这个方法,
static final Node EXCLUSIVE = null;
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; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
这个方法判断队列是否为空,如果为空,执行enq()方法,不为空,执行上面得方法,最终得结果都是将当前线程假如到等待队列中。然后acquireQueued()这个方法一层一层得,我没有看太懂,这里也就不说太多,功能其实就是让等待队列前面得获取锁。好了,整个FairSync的lock()方法已经介绍完了,那么NonFairSync的lock()方法有什么区别呢?
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
从代码中可以看到,它不会去管等待队列什么的,而是直接执行CAS操作,如果失败了,好吧,失败了大不了我就用FairSync的那一套咯。
然后我们再来看看unlock()方法。
public void unlock() { sync.release(1); }
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
从代码可以看出,会执行tryRelease()方法,如果成功,并且等待队列不为空的话,唤醒队列中第一个等待的线程。
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
因为重入锁可以多次加锁,因此只有当c为0时,才能返回true。否则,返回false。也就是说,lock几次就要unlock几次才能释放锁。
整个ReentrantLock主要的就介绍完了,这些东西是通过查看源码以及其他的博客整理出来的,整个代码的讲解也是我自己的理解,可能语言组织方面不是太好。希望自己的讲述不是太差劲。。。这篇主要整理了ReetrantLock的原理,下篇博客我准备使用ReentrantLock来实现循环打印数字及字母。