Lock

    xiaoxiao2021-03-25  116

    Lock接口方法

    相对于Synchronize,Lock在获取锁的操作上提供了阻塞、非阻塞可中断、超时设置等机制

    Lock使用方式

      使用Lock需要显式的加锁和解锁

    解锁操作需要放在finally块里,防止锁被超界获取另外,获取锁的操作不能放在try块里,因为Lock是可重入锁,如果外层也已经调用lock()方法,而里层因为调用lock()抛出异常然后调用unlock,外层无法知晓,导致外层代码无法正确同步。 Lock lock = ...; lock.lock(); try { // access the resource protected by this lock } finally { lock.unlock(); }

    Lock与Synchronize的性能对比

      在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步

    Lock实现原理概述

      Lock接口的实现基本都是通过聚合一个同步器的子类来完成线程访问控制的。当调用Lock接口方法时,Lock的实现会将实现代理给同步器的子类,如下代码示例。

    import java.io.IOException; import java.io.ObjectInputStream; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; class Mutex implements Lock, java.io.Serializable { // 实现同步器的子类 private static class Sync extends AbstractQueuedSynchronizer { // 报告是否处于锁定状态 protected boolean isHeldExclusively() { return getState() == 1; } // 如果state为0,则获取锁 public boolean tryAcquire(int acquires) { assert acquires == 1; // Otherwise unused if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 通过将state设置为0来释放锁 protected boolean tryRelease(int releases) { assert releases == 1; // Otherwise unused if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // 提供Condition实例 Condition newCondition() { return new ConditionObject(); } // 反序列化 private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // 复位到解锁状态 } } // sync实现所有的工作,只需要将实现代理给它 private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } }

    AbstractQueuedSynchronizer同步器

    实现目标

      分别在共享、独占两种模式下,实现阻塞、非阻塞、可中断、可超时的同步状态争夺

    实现思路

    一个锁的实现中涉及到两个实体:锁和线程 1. 维护锁的状态(维护同步状态:决定线程是否能获得锁、获取锁和释放锁) 2. 维护争夺锁的所有线程的状态(保存当前成功获取锁的线程,挂起和唤醒获取锁失败的线程)

    实现概述

      AbstractQueuedSynchronizer使用FIFO队列维护争夺锁的所有线程的状态,使用int类型保存同步状态。基于这两者,此类支持独占和共享两种模式,并定义相应的钩子方法(维护int同步状态变量,根据当前同步状态最终决定在两种模式下是否能成功获得锁,解决了实现思路中的第一点)供子类实现,子类可以根据需要实现相应模式的方法,ReadWriteLock类同时支持这两种模式。   AbstractQueuedSynchronizer使用模板模式,子类必须定义它的protected方法(上述的钩子方法)以检查或改变同步状态(通过调用方法getState(),setState()和compareAndSetState()),这些方法实现了共享和独占两种模式下非阻塞获取、释放锁的方法,基于这些钩子方法,这个类中的其他方法执行所有的排队和阻塞机制(解决了实现思路的第二点)。   子类通常实现为非public内部类。

    方法

      protected方法需要在子类中实现,这些方法提供了同步式和共享式的非阻塞获取、释放同步状态的方法,以及查看当前同步器是否被线程占用的方法。   这些protected方法默认抛出UnsupportedOperationException异常,这些方法的实现必须是线程安全的,并且必须是非阻塞的,仅支持独占或仅共享模式的子类不需要定义支持未使用模式的方法,ReadWriteLock类同时实现了两种模式。   protected方法如下图。   

      其他方法在protected方法的基础上,分别对共享式和独占式获取锁的方法提供了阻塞、中断、超时特性,并且额外添加获取同步队列线程集合等特性,这些方法要么是public final类型,要么是private类型,都为不可重写方法。部分方法如下图所示。   

    同步队列

      在不同模式下等待的线程共享同一个FIFO队列(注意是共享同一个),用以实现对等待线程的管理。   同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列尾部,同时会阻塞当前线程;当同步状态释放时,会把首节点的线程唤醒,使其再次尝试获取同步状态。   同步队列中的节点(Node)用于保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。如下图所示。

      同步器拥有同步队列的首节点(head)和尾节点(tail)的引用。   

    独占式不可中断同步状态获取与释放

    acquire(int arg):获取同步状态,不可中断。 实现逻辑:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

    public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

    tryAcquire(int):由子类实现,用于非阻塞并且线程安全地获取同步状态,默认实现中抛出异常

    protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }

    addWaiter(Node):构造节点,并将节点添加到同步队列的尾部,从enq()方法中可知,第一个入队的节点只是通过new Node()方式创建,该节点不持有任何线程

    private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 快速入队,如果入队失败,则调用enq()方法循环尝试入队,直到成功为止 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); }

    acquireQueued(Node,int):进入自旋,当获取到同步状态,就从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞线程)

    /** * 被已经位于队列中的节点线程调用,在独占式非中断模式下获取锁。 * 由acquire方法和Condition的wait方法调用。 * / 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) //设置当前节点ws状态为cancelled,如果存在后继节点,则唤醒后继节点或者将其接上队列前面的节点 cancelAcquire(node); } } /** * Checks and updates status for a node that failed to acquire. * Returns true if thread should block. This is the main signal * control in all acquire loops. Requires that pred == node.prev. * * @param pred node's predecessor holding status * @param node the node * @return {@code true} if thread should block */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; //SIGNAL状态:前驱节点释放同步状态或者被取消,将会通知后继节点。因此,可以放心的阻塞当前线程,返回true。 if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; // help GC } }

      在acquireQueued(final Node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。原因有两个,如下。

    头结点是成功获取同步状态的节点,而头结点的线程释放锁以后,将唤醒后继节点,后继节点线程被唤醒后要检查自己的前驱节点是否为头结点。维护同步队列的FIFO原则。

    整个获取锁的流程如下图。

    acquire(int arg)方法执行逻辑

         由上图可知,在acquire()方法执行中,第一次调用tryAcquire(int)成功返回的线程不会入队,第一次调用tryAcquire(int)失败返回的线程全部都需要入队。   在入队之后,线程会进入自旋过程。   当自旋失败并且前一个节点waitStatus为signal时,线程会进入阻塞状态,等待持有锁的线程调用release()时被唤醒。   当自旋成功,当前线程所在节点会被设置为头节点。 release(int arg):独占式的释放锁,只负责唤醒后继节点,不删除也不增加队列节点

    public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } /** * 唤醒后继节点 */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * unpark后续节点,通常是下一个节点。但如果next是null或者canceled节点,则从队尾 * 往前寻找最前的一个非canceled节点 */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

    共享式不可中断同步状态获取与释放

    /** * 共享式、不可中断地获取同步状态 */ public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

    LockSupport工具

      当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。   LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开,这些方法及描述如下。

    方法名称描述void park()阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回void parkNanos(long nanos)阻塞当前线程,最长不超过nanos,返回条件在park()的基础上增加了超时返回void parkUntil(long deadline)阻塞当前线程,直到deadline(从1970年开始到deadline时间的毫秒数)void unpark(Thread thread)唤醒处于阻塞状态的线程thread

         在Java6中,LockSupport增加了park(Object blocker),parkNanos(Object blocker, long nanos)和parkUntil(Object blocker, long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象(以下称为阻塞对象)该对象主要用于问题排查和系统监控。   

      从上表的线程dump结果可以看出,代码片段的内容都是阻塞当前线程10秒,但从线程dump结果可以看出,有阻塞对象的parkNanos方法能够传递给开发人员更多的现象信息。这是由于在Java5之前,当前线程阻塞(使用synchronized)在一个对象上时,通过线程dump能够查看到该线程阻塞的对象,方便问题定位,而Java5推出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息,因此,在Java6中,LockSupport新增了上述3个含有阻塞对象的park方法,用以替代原有的park方法。

    Condition

    方法

    实现分析

      每一个Condition对象都包含一个等待队列(FIFO队列),并拥有首节点(firstWaiter)和尾节点(lastWaiter)的引用,在队列的每一个节点(和同步器中的同步队列共用一个Node类)包含一个线程引用,该线程就是在Condition对象上等待的线程

      

      在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(确切地说是同步器)拥有一个同步队列和多个等待队列。

      

    /** * Implements interruptible condition wait. * <ol> * <li> If current thread is interrupted, throw InterruptedException. * <li> Save lock state returned by {@link #getState}. * <li> Invoke {@link #release} with saved state as argument, * throwing IllegalMonitorStateException if it fails. * <li> Block until signalled or interrupted. * <li> Reacquire by invoking specialized version of * {@link #acquire} with saved state as argument. * <li> If interrupted while blocked in step 4, throw InterruptedException. * </ol> */ public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter();//向等待队列添加节点 int savedState = fullyRelease(node);//释放锁并唤醒同步队列后继节点 int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } /** * 添加节点到等待队列 * @return its new wait node */ private Node addConditionWaiter() { Node t = lastWaiter; // 如果lastWaiter是cancelled状态, 执行队列清理 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } //创建新节点,CONDITION状态表示节点线程等待在Condition对象上 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } /** * 移除等待队列中的cancelled(非CONDITION)节点 */ private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }

      如果一个线程调用了Condition.await()方法,该线程将会释放锁、构造节点加入等待队列的尾部并进入等待状态。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。   

    /** * 将在等待队列中等待时间最长的节点(首节点)加入到同步队列中,并唤醒节点线程 * * @throws IllegalMonitorStateException if {@link #isHeldExclusively} * returns {@code false} */ public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }

      被唤醒后的线程,将从await()方法中的while循环退出(isOnSyncQueue(Node node)方法返回true,表示节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。   成功获取同步状态后,被唤醒的线程将从先前调用的await()方法返回,此时线程已经成功获取到了锁。   Conditon的signalAll()相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点移动到同步队列中,并唤醒每个节点的线程。   

    转载请注明原文地址: https://ju.6miu.com/read-32957.html

    最新回复(0)