From 49a094e7d8e6f98fb96befcd2fb50512a5261cb0 Mon Sep 17 00:00:00 2001 From: dbcxyxd <43689452+dbcxyxd@users.noreply.github.com> Date: Sun, 29 Dec 2019 20:47:12 +0800 Subject: [PATCH] week3_51 --- week_03/51/AQS_51.md | 590 +++++++++++ ...05\345\255\230\346\250\241\345\236\213.md" | 235 +++++ week_03/51/ReentrantLock_51.md | 679 +++++++++++++ week_03/51/Semaphore_51.md | 250 +++++ week_03/51/Semaphore_51_1.md | 386 ++++++++ week_03/51/synchronized_51.md | 912 ++++++++++++++++++ week_03/51/volatile_51.md | 69 ++ week_03/51/volatile_51_2.md | 334 +++++++ 8 files changed, 3455 insertions(+) create mode 100644 week_03/51/AQS_51.md create mode 100644 "week_03/51/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" create mode 100644 week_03/51/ReentrantLock_51.md create mode 100644 week_03/51/Semaphore_51.md create mode 100644 week_03/51/Semaphore_51_1.md create mode 100644 week_03/51/synchronized_51.md create mode 100644 week_03/51/volatile_51.md create mode 100644 week_03/51/volatile_51_2.md diff --git a/week_03/51/AQS_51.md b/week_03/51/AQS_51.md new file mode 100644 index 0000000..2bcdc1a --- /dev/null +++ b/week_03/51/AQS_51.md @@ -0,0 +1,590 @@ +AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。 + +![img](https:////upload-images.jianshu.io/upload_images/53727-ae36db58241c256b.png?imageMogr2/auto-orient/strip|imageView2/2/w/852/format/webp) + +AQS.png + + +   AbstractQueuedSynchronizer中对state的操作是原子的,且不能被继承。所有的同步机制的实现均依赖于对改变量的原子操作。为了实现不同的同步机制,我们需要创建一个非共有的(non-public internal)扩展了AQS类的内部辅助类来实现相应的同步逻辑。AbstractQueuedSynchronizer并不实现任何同步接口,它提供了一些可以被具体实现类直接调用的一些原子操作方法来重写相应的同步逻辑。AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如`ReadWriteLock`。接下来将详细介绍AbstractQueuedSynchronizer的提供的一些具体实现方法。 + + + +#### state状态 + +  AbstractQueuedSynchronizer维护了一个volatile int类型的变量,用户表示当前同步状态。volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性。至于[volatile](https://www.jianshu.com/p/14fc9d34de33)的具体语义,可以参考相关文章。state的访问方式有三种: + +> - getState() +> - setState() +> - compareAndSetState() + +  这三种叫做均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。代码实现如下: + + + +```java + /** + * The synchronization state. + */ + private volatile int state; + + /** + * Returns the current value of synchronization state. + * This operation has memory semantics of a {@code volatile} read. + * @return current state value + */ + protected final int getState() { + return state; + } + + /** + * Sets the value of synchronization state. + * This operation has memory semantics of a {@code volatile} write. + * @param newState the new state value + */ + protected final void setState(int newState) { + state = newState; + } + + /** + * Atomically sets synchronization state to the given updated + * value if the current state value equals the expected value. + * This operation has memory semantics of a {@code volatile} read + * and write. + * + * @param expect the expected value + * @param update the new value + * @return {@code true} if successful. False return indicates that the actual + * value was not equal to the expected value. + */ + protected final boolean compareAndSetState(int expect, int update) { + // See below for intrinsics setup to support this + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); + } +``` + +#### 自定义资源共享方式 + +  AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。 +  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法: + +> - isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。 +> - tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。 +> - tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。 +> - tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +> - tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。 + +#### 源码实现 + +  接下来我们开始开始讲解AQS的源码实现。依照acquire-release、acquireShared-releaseShared的次序来。 + +#### 1. acquire(int) + +    acquire是一种以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。该方法是独占模式下线程获取共享资源的顶层入口。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码: + + + +```dart +/** + * 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(); + } +``` + +  通过注释我们知道,acquire方法是一种互斥模式,且忽略中断。该方法至少执行一次`tryAcquire(int)`方法,如果tryAcquire(int)方法返回true,则acquire直接返回,否则当前线程需要进入队列进行排队。函数流程如下: + +> 1. tryAcquire()尝试直接去获取资源,如果成功则直接返回; +> 2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式; +> 3. acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 +> 4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。 + +接下来一次介绍相关方法。 + +##### 1.1 tryAcquire(int) + +   tryAcquire尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false。该方法可以用于实现Lock中的tryLock()方法。该方法的默认实现是抛出`UnsupportedOperationException`,具体实现由自定义的扩展了AQS的同步类来实现。AQS在这里只负责定义了一个公共的方法框架。这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。 + + + +```dart +/** + * Attempts to acquire in exclusive mode. This method should query + * if the state of the object permits it to be acquired in the + * exclusive mode, and if so to acquire it. + * + *

This method is always invoked by the thread performing + * acquire. If this method reports failure, the acquire method + * may queue the thread, if it is not already queued, until it is + * signalled by a release from some other thread. This can be used + * to implement method {@link Lock#tryLock()}. + * + *

The default + * implementation throws {@link UnsupportedOperationException}. + * + * @param arg the acquire argument. This value is always the one + * passed to an acquire method, or is the value saved on entry + * to a condition wait. The value is otherwise uninterpreted + * and can represent anything you like. + * @return {@code true} if successful. Upon success, this object has + * been acquired. + * @throws IllegalMonitorStateException if acquiring would place this + * synchronizer in an illegal state. This exception must be + * thrown in a consistent fashion for synchronization to work + * correctly. + * @throws UnsupportedOperationException if exclusive mode is not supported + */ + protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); + } +``` + +##### 1.2 addWaiter(Node) + +  该方法用于将当前线程根据不同的模式(`Node.EXCLUSIVE`互斥模式、`Node.SHARED`共享模式)加入到等待队列的队尾,并返回当前线程所在的结点。如果队列不为空,则以通过`compareAndSetTail`方法以CAS的方式将当前线程节点加入到等待队列的末尾。否则,通过enq(node)方法初始化一个等待队列,并返回当前节点。源码如下: + + + +```java +/** + * 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; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; + } +``` + +##### 1.2.1 enq(node) + +  `enq(node)`用于将当前节点插入等待队列,如果队列为空,则初始化当前队列。整个过程以CAS自旋的方式进行,直到成功加入队尾为止。源码如下: + + + +```java +/** + * Inserts node into queue, initializing if necessary. See picture above. + * @param node the node to insert + * @return node's predecessor + */ + private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } + } +``` + +##### 1.3 acquireQueued(Node, int) + +  `acquireQueued()`用于队列中的线程自旋地以独占且不可中断的方式获取同步状态(acquire),直到拿到锁之后再返回。该方法的实现分成两部分:如果当前节点已经成为头结点,尝试获取锁(tryAcquire)成功,然后返回;否则检查当前节点是否应该被park,然后将该线程park并且检查当前线程是否被可以被中断。 + + + +```java +/** + * 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) { + //标记是否成功拿到资源,默认false + 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); + } + } +``` + +##### 1.3.1 shouldParkAfterFailedAcquire(Node, Node) + +  shouldParkAfterFailedAcquire方法通过对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作,至于每个Node的状态表示,可以参考接口文档。 + + + +```java +/** + * 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; + 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; + } +``` + +##### 1.3.2 parkAndCheckInterrupt() + +  该方法让线程去休息,真正进入等待状态。park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。 + + + +```java +/** + * Convenience method to park and then check if interrupted + * + * @return {@code true} if interrupted + */ + private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); + } +``` + +  我们再回到acquireQueued(),总结下该函数的具体流程: + +> 1. 结点进入队尾后,检查状态,找到安全休息点; +> 2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己; +> 3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。 + +最后,总结一下acquire()的流程: + +> 1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回; +> 2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式; +> 3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 +> 4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。 + +#### 2. release(int) + +  `release(int)`方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码: + + + +```dart +/** + * Releases in exclusive mode. Implemented by unblocking one or + * more threads if {@link #tryRelease} returns true. + * This method can be used to implement method {@link Lock#unlock}. + * + * @param arg the release argument. This value is conveyed to + * {@link #tryRelease} but is otherwise uninterpreted and + * can represent anything you like. + * @return the value returned from {@link #tryRelease} + */ + public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; + } + +/** + * Attempts to set the state to reflect a release in exclusive + * mode. + * + *

This method is always invoked by the thread performing release. + * + *

The default implementation throws + * {@link UnsupportedOperationException}. + * + * @param arg the release argument. This value is always the one + * passed to a release method, or the current state value upon + * entry to a condition wait. The value is otherwise + * uninterpreted and can represent anything you like. + * @return {@code true} if this object is now in a fully released + * state, so that any waiting threads may attempt to acquire; + * and {@code false} otherwise. + * @throws IllegalMonitorStateException if releasing would place this + * synchronizer in an illegal state. This exception must be + * thrown in a consistent fashion for synchronization to work + * correctly. + * @throws UnsupportedOperationException if exclusive mode is not supported + */ + protected boolean tryRelease(int arg) { + throw new UnsupportedOperationException(); + } + +/** + * Wakes up node's successor, if one exists. + * + * @param node the node + */ + 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); + + /* + * Thread to unpark is held in successor, which is normally + * just the next node. But if cancelled or apparently null, + * traverse backwards from tail to find the actual + * non-cancelled successor. + */ + 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); + } +``` + +  与acquire()方法中的tryAcquire()类似,tryRelease()方法也是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。 +  `unparkSuccessor(Node)`方法用于唤醒等待队列中下一个线程。这里要注意的是,下一个线程并不一定是当前节点的next节点,而是下一个可以用来唤醒的线程,如果这个节点存在,调用`unpark()`方法唤醒。 +  总之,release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。 + +#### 3. acquireShared(int) + +  `acquireShared(int)`方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码: + + + +```dart +/** + * Acquires in shared mode, ignoring interrupts. Implemented by + * first invoking at least once {@link #tryAcquireShared}, + * returning on success. Otherwise the thread is queued, possibly + * repeatedly blocking and unblocking, invoking {@link + * #tryAcquireShared} until success. + * + * @param arg the acquire argument. This value is conveyed to + * {@link #tryAcquireShared} but is otherwise uninterpreted + * and can represent anything you like. + */ + public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); + } +``` + +##### 3.1 doAcquireShared(int) + +  将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。源码如下: + + + +```java + /** + * Acquires in shared uninterruptible mode. + * @param arg the acquire argument + */ + 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); + } + } +``` + +  跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。实现如下: + + + +```dart +/** + * Sets head of queue, and checks if successor may be waiting + * in shared mode, if so propagating if either propagate > 0 or + * PROPAGATE status was set. + * + * @param node the node + * @param propagate the return value from a tryAcquireShared + */ + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; // Record old head for check below + setHead(node); + /* + * Try to signal next queued node if: + * Propagation was indicated by caller, + * or was recorded (as h.waitStatus either before + * or after setHead) by a previous operation + * (note: this uses sign-check of waitStatus because + * PROPAGATE status may transition to SIGNAL.) + * and + * The next node is waiting in shared mode, + * or we don't know, because it appears null + * + * The conservatism in both of these checks may cause + * unnecessary wake-ups, but only when there are multiple + * racing acquires/releases, so most need signals now or soon + * anyway. + */ + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + if (s == null || s.isShared()) + doReleaseShared(); + } + } +``` + +  此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!至此,acquireShared()也要告一段落了。让我们再梳理一下它的流程: + +> 1. tryAcquireShared()尝试获取资源,成功则直接返回; +> 2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。 + +#### 4. releaseShared(int) + +  `releaseShared(int)`方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码: + + + +```java +/** + * Releases in shared mode. Implemented by unblocking one or more + * threads if {@link #tryReleaseShared} returns true. + * + * @param arg the release argument. This value is conveyed to + * {@link #tryReleaseShared} but is otherwise uninterpreted + * and can represent anything you like. + * @return the value returned from {@link #tryReleaseShared} + */ + public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; + } +``` + +  此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。 + + + +```csharp +/** + * Release action for shared mode -- signals successor and ensures + * propagation. (Note: For exclusive mode, release just amounts + * to calling unparkSuccessor of head if it needs signal.) + */ + private void doReleaseShared() { + /* + * Ensure that a release propagates, even if there are other + * in-progress acquires/releases. This proceeds in the usual + * way of trying to unparkSuccessor of head if it needs + * signal. But if it does not, status is set to PROPAGATE to + * ensure that upon release, propagation continues. + * Additionally, we must loop in case a new node is added + * while we are doing this. Also, unlike other uses of + * unparkSuccessor, we need to know if CAS to reset status + * fails, if so rechecking. + */ + for (;;) { + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; + if (ws == Node.SIGNAL) { + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; // loop to recheck cases + unparkSuccessor(h); + } + else if (ws == 0 && + !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; // loop on failed CAS + } + if (h == head) // loop if head changed + break; + } + } +``` \ No newline at end of file diff --git "a/week_03/51/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/week_03/51/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 0000000..7409dc7 --- /dev/null +++ "b/week_03/51/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,235 @@ +## 简介 + +Java内存模型是在硬件内存模型上的更高层的抽象,它屏蔽了各种硬件和操作系统访问的差异性,保证了Java程序在各种平台下对内存的访问都能达到一致的效果。 + +## 硬件内存模型 + +在正式讲解Java的内存模型之前,我们有必要先了解一下硬件层面的一些东西。 + +在现代计算机的硬件体系中,CPU的运算速度是非常快的,远远高于它从存储介质读取数据的速度,这里的存储介质有很多,比如磁盘、光盘、网卡、内存等,这些存储介质有一个很明显的特点——距离CPU越近的存储介质往往越小越贵越快,距离CPU越远的存储介质往往越大越便宜越慢。 + +所以,在程序运行的过程中,CPU大部分时间都浪费在了磁盘IO、网络通讯、数据库访问上,如果不想让CPU在那里白白等待,我们就必须想办法去把CPU的运算能力压榨出来,否则就会造成很大的浪费,而让CPU同时去处理多项任务则是最容易想到的,也是被证明非常有效的压榨手段,这也就是我们常说的“并发执行”。 + +但是,让CPU并发地执行多项任务并不是那么容易实现的事,因为所有的运算都不可能只依靠CPU的计算就能完成,往往还需要跟内存进行交互,如读取运算数据、存储运算结果等。 + +前面我们也说过了,CPU与内存的交互往往是很慢的,所以这就要求我们要想办法在CPU和内存之间建立一种连接,使它们达到一种平衡,让运算能快速地进行,而这种连接就是我们常说的“高速缓存”。 + +高速缓存的速度是非常接近CPU的,但是它的引入又带来了新的问题,现代的CPU往往是有多个核心的,每个核心都有自己的缓存,而多个核心之间是不存在时间片的竞争的,它们可以并行地执行,那么,怎么保证这些缓存与主内存中的数据的一致性就成为了一个难题。 + +为了解决缓存一致性的问题,多个核心在访问缓存时要遵循一些协议,在读写操作时根据协议来操作,这些协议有MSI、MESI、MOSI等,它们定义了何时应该访问缓存中的数据、何时应该让缓存失效、何时应该访问主内存中的数据等基本原则。 + +![img](https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3xbDJsntH0PEMI2Yl8RfVK8TOic3SLeAT6YSCDD0L7s1HEL02SnsuUCzNd3zX6HTLCajYg8EiathAkA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +而随着CPU能力的不断提升,一层缓存就无法满足要求了,就逐渐衍生出了多级缓存。 + +按照数据读取顺序和CPU的紧密程度,CPU的缓存可以分为一级缓存(L1)、二级缓存(L2)、三级缓存(L3),每一级缓存存储的数据都是下一级的一部分。 + +这三种缓存的技术难度和制作成本是相对递减的,容量也是相对递增的。 + +所以,在有了多级缓存后,程序的运行就变成了: + +当CPU要读取一个数据的时候,先从一级缓存中查找,如果没找到再从二级缓存中查找,如果没找到再从三级缓存中查找,如果没找到再从主内存中查找,然后再把找到的数据依次加载到多级缓存中,下次再使用相关的数据直接从缓存中查找即可。 + +而加载到缓存中的数据也不是说用到哪个就加载哪个,而是加载内存中连续的数据,一般来说是加载连续的64个字节,因此,如果访问一个 long 类型的数组时,当数组中的一个值被加载到缓存中时,另外 7 个元素也会被加载到缓存中,这就是“缓存行”的概念。 + +![img](https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3xbDJsntH0PEMI2Yl8RfVK83Rx4gf8B0srIzTmyygFvYRmhTMhYzBbUCYcia7kiaNZFcC4n1oEVZkCQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +缓存行虽然能极大地提高程序运行的效率,但是在多线程对共享变量的访问过程中又带来了新的问题,也就是非常著名的“伪共享”。 + +关于伪共享的问题,我们这里就不展开讲了,有兴趣的可以看彤哥之前发布的【[杂谈 什么是伪共享(false sharing)?](https://mp.weixin.qq.com/s?__biz=Mzg2ODA0ODM0Nw==&mid=2247483887&idx=1&sn=eac830409917a8c31840c687b4a4154b&scene=21#wechat_redirect)】章节的相关内容。 + +除此之外,为了使CPU中的运算单元能够充分地被利用,CPU可能会对输入的代码进行乱序执行优化,然后在计算之后再将乱序执行的结果进行重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与代码的输入顺序一致,因此,如果一个计算任务依赖于另一个计算任务的结果,那么其顺序性并不能靠代码的先后顺序来保证。 + +与CPU的乱序执行优化类似,java虚拟机的即时编译器也有类似的指令重排序优化。 + +为了解决上面提到的多个缓存读写一致性以及乱序排序优化的问题,这就有了内存模型,它定义了共享内存系统中多线程读写操作行为的规范。 + +## Java内存模型 + +Java内存模型(Java Memory Model,JMM)是在硬件内存模型基础上更高层的抽象,它屏蔽了各种硬件和操作系统对内存访问的差异性,从而实现让Java程序在各种平台下都能达到一致的并发效果。 + +Java内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。这里所说的变量包括实例字段、静态字段,但不包括局部变量和方法参数,因为它们是线程私有的,它们不会被共享,自然不存在竞争问题。 + +为了获得更好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码的执行顺序等这类权利。 + +Java内存模型规定了所有的变量都存储在主内存中,这里的主内存跟介绍硬件时所用的名字一样,两者可以类比,但此处仅指虚拟机中内存的一部分。 + +除了主内存,每条线程还有自己的工作内存,此处可与CPU的高速缓存进行类比。工作内存中保存着该线程使用到的变量的主内存副本的拷贝,线程对变量的操作都必须在工作内存中进行,包括读取和赋值等,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存来完成。 + +线程、工作内存、主内存三者的关系如下图所示: + +![img](https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3xbDJsntH0PEMI2Yl8RfVK8Gs8jZ6wXUBoOMQKUiac4WvjryngZRClicpU6KdS2aXiaUNqDpn4WasIaA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)u + +注意,这里所说的主内存、工作内存跟Java虚拟机内存区域划分中的堆、栈是不同层次的内存划分,如果两者一定要勉强对应起来,主内存主要对应于堆中对象的实例部分,而工作内存主要对应与虚拟机栈中的部分区域。 + +从更低层次来说,主内存主要对应于硬件内存部分,工作内存主要对应于CPU的高速缓存和寄存器部分,但也不是绝对的,主内存也可能存在于高速缓存和寄存器中,工作内存也可能存在于硬件内存中。 + +![img](https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3xbDJsntH0PEMI2Yl8RfVK8mECTb5oylANa1SEohZB65FPBSp7LIIic5iat7ic54m3KNABeib6qOepicdA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +## 内存间的交互操作 + +关于主内存与工作内存之间具体的交互协议,Java内存模型定义了以下8种具体的操作来完成: + +(1)lock,锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态; + +(2)unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定; + +(3)read,读取,作用于主内存的变量,它把一个变量从主内存传输到工作内存中,以便后续的load操作使用; + +(4)load,载入,作用于工作内存的变量,它把read操作从主内存得到的变量放入工作内存的变量副本中; + +(5)use,使用,作用于工作内存的变量,它把工作内存中的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作; + +(6)assign,赋值,作用于工作内存的变量,它把一个从执行引擎接收到的变量赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时使用这个操作; + +(7)store,存储,作用于工作内存的变量,它把工作内存中一个变量的值传递到主内存中,以便后续的write操作使用; + +(8)write,写入,作用于主内存的变量,它把store操作从工作内存得到的变量的值放入到主内存的变量中; + +如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作,同样地,如果要把一个变量从工作内存同步回主内存,就要按顺序地执行store和write操作。注意,这里只说明了要按顺序,并没有说一定要连续,也就是说可以在read与load之间、store与write之间插入其它操作。比如,对主内存中的变量a和b的访问,可以按照以下顺序执行: + +read a -> read b -> load b -> load a。 + +另外,Java内存模型还定义了执行上述8种操作的基本规则: + +(1)不允许read和load、store和write操作之一单独出现,即不允许出现从主内存读取了而工作内存不接受,或者从工作内存回写了但主内存不接受的情况出现; + +(2)不允许一个线程丢弃它最近的assign操作,即变量在工作内存变化了必须把该变化同步回主内存; + +(3)不允许一个线程无原因地(即未发生过assign操作)把一个变量从工作内存同步回主内存; + +(4)一个新的变量必须在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load或assign)过的变量,换句话说就是对一个变量的use和store操作之前必须执行过load和assign操作; + +(5)一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。 + +(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值; + +(7)如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量; + +(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作; + +注意,这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即moniterenter和moniterexit。 + +## 原子性、可见性、有序性 + +Java内存模型就是为了解决多线程环境下共享变量的一致性问题,那么一致性包含哪些内容呢? + +一致性主要包含三大特性:原子性、可见性、有序性,下面我们就来看看Java内存模型是怎么实现这三大特性的。 + +(1)原子性 + +原子性是指一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是一个操作,也可以是多个操作。 + +由Java内存模型来直接保证的原子性操作包括read、load、user、assign、store、write这两个操作,我们可以大致认为基本类型变量的读写是具备原子性的。 + +如果应用需要一个更大范围的原子性,Java内存模型还提供了lock和unlock这两个操作来满足这种需求,尽管不能直接使用这两个操作,但我们可以使用它们更具体的实现synchronized来实现。 + +因此,synchronized块之间的操作也是原子性的。 + +(2)可见性 + +可见性是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。 + +Java内存模型是通过在变更修改后同步回主内存,在变量读取前从主内存刷新变量值来实现的,它是依赖主内存的,无论是普通变量还是volatile变量都是如此。 + +普通变量与volatile变量的主要区别是是否会在修改之后立即同步回主内存,以及是否在每次读取前立即从主内存刷新。因此我们可以说volatile变量保证了多线程环境下变量的可见性,但普通变量不能保证这一点。 + +除了volatile之外,还有两个关键字也可以保证可见性,它们是synchronized和final。 + +synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作”这条规则获取的。 + +final的可见性是指被final修饰的字段在构造器中一旦被初始化完成,那么其它线程中就能看见这个final字段了。 + +(3)有序性 + +Java程序中天然的有序性可以总结为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。 + +前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。 + +Java中提供了volatile和synchronized两个关键字来保证有序性。 + +volatile天然就具有有序性,因为其禁止重排序。 + +synchronized的有序性是由“一个变量同一时刻只允许一条线程对其进行lock操作”这条规则获取的。 + +## 先行发生原则(Happens-Before) + +如果Java内存模型的有序性都只依靠volatile和synchronized来完成,那么有一些操作就会变得很啰嗦,但是我们在编写Java并发代码时并没有感受到,这是因为Java语言天然定义了一个“先行发生”原则,这个原则非常重要,依靠这个原则我们可以很容易地判断在并发环境下两个操作是否可能存在竞争冲突问题。 + +先行发生,是指操作A先行发生于操作B,那么操作A产生的影响能够被操作B感知到,这种影响包括修改了共享内存中变量的值、发送了消息、调用了方法等。 + +下面我们看看Java内存模型定义的先行发生原则有哪些: + +(1)程序次序原则 + +在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确地讲是控制流顺序而不是代码顺序,因为要考虑分支、循环等情况。 + +(2)监视器锁定原则 + +一个unlock操作先行发生于后面对同一个锁的lock操作。 + +(3)volatile原则 + +对一个volatile变量的写操作先行发生于后面对该变量的读操作。 + +(4)线程启动原则 + +对线程的start()操作先行发生于线程内的任何操作。 + +(5)线程终止原则 + +线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止。 + +(6)线程中断原则 + +对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断。 + +(7)对象终结原则 + +一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始。 + +(8)传递性原则 + +如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。 + +这里说的“先行发生”与“时间上的先发生”没有必然的关系。 + +比如,下面的代码: + +``` +int a = 0; +// 操作A:线程1对进行赋值操作a = 1; +// 操作B:线程2获取a的值 +int b = a; +``` + +如果线程1在时间顺序上先对a进行赋值,然后线程2再获取a的值,这能说明操作A先行发生于操作B吗? + +显然不能,因为线程2可能读取的还是其工作内存中的值,或者说线程1并没有把a的值刷新回主内存呢,这时候线程2读取到的值可能还是0。 + +所以,“时间上的先发生”不一定“先行发生”。 + +再看一个例子: + +``` +// 同一个线程中int i = 1; +int j = 2; +``` + +根据第一条程序次序原则, `inti=1;`先行发生于 `intj=2;`,但是由于处理器优化,可能导致 `intj=2;`先执行,但是这并不影响先行发生原则的正确性,因为我们在这个线程中并不会感知到这点。 + +所以,“先行发生”不一定“时间上先发生”。 + +## 总结 + +(1)硬件内存架构使得我们必须建立内存模型来保证多线程环境下对共享内存访问的正确性; + +(2)Java内存模型定义了保证多线程环境下共享变量一致性的规则; + +(3)Java内存模型提供了工作内存与主内存交互的8大操作:lock、unlock、read、load、use、assign、store、write; + +(4)Java内存模型对原子性、可见性、有序性提供了一些实现; + +(5)先行发生的8大原则:程序次序原则、监视器锁定原则、volatile原则、线程启动原则、线程终止原则、线程中断原则、对象终结原则、传递性原则; + +(6)先行发生不等于时间上的先发生; \ No newline at end of file diff --git a/week_03/51/ReentrantLock_51.md b/week_03/51/ReentrantLock_51.md new file mode 100644 index 0000000..a0022ed --- /dev/null +++ b/week_03/51/ReentrantLock_51.md @@ -0,0 +1,679 @@ +目录 + +- [1.前言](https://www.cnblogs.com/takumicx/p/9402021.html#前言) +- 2.AbstractQueuedSynchronizer介绍 + - [2.1 AQS是构建同步组件的基础](https://www.cnblogs.com/takumicx/p/9402021.html#aqs是构建同步组件的基础) + - [2.2 AQS的内部结构(ReentrantLock的语境下)](https://www.cnblogs.com/takumicx/p/9402021.html#aqs的内部结构reentrantlock的语境下) +- 3 非公平模式加锁流程 + - [3.1加锁流程真正意义上的入口](https://www.cnblogs.com/takumicx/p/9402021.html#加锁流程真正意义上的入口) + - [3.2 尝试获取锁的通用方法:tryAcquire()](https://www.cnblogs.com/takumicx/p/9402021.html#尝试获取锁的通用方法tryacquire) + - [3.3 获取锁失败的线程如何安全的加入同步队列:addWaiter()](https://www.cnblogs.com/takumicx/p/9402021.html#获取锁失败的线程如何安全的加入同步队列addwaiter) + - [3.4 线程加入同步队列后会做什么:acquireQueued()](https://www.cnblogs.com/takumicx/p/9402021.html#线程加入同步队列后会做什么acquirequeued) + - [3.5 加锁流程源码总结](https://www.cnblogs.com/takumicx/p/9402021.html#加锁流程源码总结) +- 4.非公平模式解锁流程 + - [4.1 解锁流程源码解读](https://www.cnblogs.com/takumicx/p/9402021.html#解锁流程源码解读) + - [4.2 解锁流程源码总结](https://www.cnblogs.com/takumicx/p/9402021.html#解锁流程源码总结) +- [5. 公平锁相比非公平锁的不同](https://www.cnblogs.com/takumicx/p/9402021.html#公平锁相比非公平锁的不同) +- \6. 一些疑问的解答 + - [6.1 为什么基于FIFO的同步队列可以实现非公平锁?](https://www.cnblogs.com/takumicx/p/9402021.html#为什么基于fifo的同步队列可以实现非公平锁) + - [6.2 为什么非公平锁性能好](https://www.cnblogs.com/takumicx/p/9402021.html#为什么非公平锁性能好) +- [7. 阅读源码的收获](https://www.cnblogs.com/takumicx/p/9402021.html#阅读源码的收获) + +## 1.前言 + +在[ReentrantLock(重入锁)功能详解和应用演示](https://www.cnblogs.com/takumicx/p/9338983.html)这篇文章里我们讲解并演示了ReentrantLock(重入锁)的各种功能,其中就谈到ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值,继续跟进下源码我们就能发现,关键在于实例化内部变量`sync`的方式不同,如下所示 + +``` +/** + * 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(); +} +``` + +公平锁内部是FairSync,非公平锁内部是NonfairSync。而不管是FairSync还是NonfariSync,都间接继承自AbstractQueuedSynchronizer这个抽象类,如下图所示 + +- NonfairSync的类继承关系 + ![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805183109489-1524063793.png) +- FairSync的类继承关系 + ![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805183410668-314295960.png) + +该抽象类为我们的加锁和解锁过程提供了统一的模板方法,只是一些细节的处理由该抽象类的实现类自己决定。所以在解读ReentrantLock(重入锁)的源码之前,有必要了解下AbstractQueuedSynchronizer。 + +## 2.AbstractQueuedSynchronizer介绍 + +### 2.1 AQS是构建同步组件的基础 + +AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805183454325-124283154.png) + +AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。 + +### 2.2 AQS的内部结构(ReentrantLock的语境下) + +AQS的内部结构主要由同步等待队列构成 + +#### 2.2.1 同步等待队列 + +AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时可以更直观形象,况且引用本身是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。 + +``` +/** + * Head of the wait queue, lazily initialized. Except for + * initialization, it is modified only via method setHead. Note: + * If head exists, its waitStatus is guaranteed not to be + * CANCELLED. + */ +private transient volatile Node head;//指向队列首元素的头指针 + +/** + * Tail of the wait queue, lazily initialized. Modified only via + * method enq to add new wait node. + */ +private transient volatile Node tail;//指向队列尾元素的尾指针 +``` + +head是头指针,指向队列的首元素;tail是尾指针,指向队列的尾元素。而队列的元素结点Node定义在AQS内部,主要有如下几个成员变量 + +``` +volatile Node prev; //指向前一个结点的指针 + +volatile Node next; //指向后一个结点的指针 +volatile Thread thread; //当前结点代表的线程 +volatile int waitStatus; //等待状态 +``` + +- prev:指向前一个结点的指针 +- next:指向后一个结点的指针 +- thread:当前结点表示的线程,因为同步队列中的结点内部封装了之前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用 +- waitStatus:对于重入锁而言,主要有3个值。0:初始化状态;-1(SIGNAL):当前结点表示的线程在释放锁后需要唤醒后续节点的线程;1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待。 + +------ + +同步队列的结构如下图所示 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805183911414-329333235.png) + +为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解: + +- 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。 +- 2.队列首结点可以用来表示当前正获取锁的线程。 +- 3.当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。 + +为了加深理解,还可以在阅读源码的过程中思考下这个问题: + +这个同步队列是FIFO队列,也就是说先在队列中等待的线程将比后面的线程更早的得到锁,那ReentrantLock是如何基于这个FIFO队列实现非公平锁的? + +#### 2.2.2 AQS中的其他数据结构(ReentrantLock的语境下) + +- 同步状态变量 + +``` +/** + * The synchronization state. + */ +private volatile int state; +``` + +这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数。当state为0表示该锁不被任何线程持有;当state为1表示线程恰好持有该锁1次(未重入);当state大于1则表示锁被线程重入state次。因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。 + +- 持有同步状态的线程标志 + +``` +/** + * The current owner of exclusive mode synchronization. + */ +private transient Thread exclusiveOwnerThread; +``` + +如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。 + +------ + +了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。由于非公平锁在实际开发中用的比较多,故以讲解非公平锁的源码为主。以下面这段对非公平锁使用的代码为例: + +``` +/** + * @author: takumiCX + * @create: 2018-08-01 + **/ +public class NoFairLockTest { + + + public static void main(String[] args) { + + //创建非公平锁 + ReentrantLock lock = new ReentrantLock(false); + + try { + + //加锁 + lock.lock(); + + //模拟业务处理用时 + TimeUnit.SECONDS.sleep(1); + + } catch (InterruptedException e) { + e.printStackTrace(); + + } finally { + //释放锁 + lock.unlock(); + } + + } +} +``` + +## 3 非公平模式加锁流程 + +加锁流程从`lock.lock()`开始 + +``` +public void lock() { + sync.lock(); +} +``` + +进入该源码,正确找到sycn的实现类后可以看到真正有内容的入口方法 + +### 3.1加锁流程真正意义上的入口 + +``` +/** + * Performs lock. Try immediate barge, backing up to normal + * acquire on failure. + */ +//加锁流程真正意义上的入口 +final void lock() { + //以cas方式尝试将AQS中的state从0更新为1 + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread());//获取锁成功则将当前线程标记为持有锁的线程,然后直接返回 + else + acquire(1);//获取锁失败则执行该方法 +} +``` + +首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行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(); +} +``` + +该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法tryAcquire(),addWaiter()和acquireQueued(),这三个方法中分别封装了加锁流程中的主要处理逻辑,理解了这三个方法到底做了哪些事情,整个加锁流程就清晰了。 + +### 3.2 尝试获取锁的通用方法:tryAcquire() + +tryAcquire是AQS中定义的钩子方法,如下所示 + +``` +protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); +} +``` + +该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现,非公平模式的实现如下: + +``` +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} +``` + +底层调用了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();//获取state变量的值,即当前锁被重入的次数 + if (c == 0) { //state为0,说明当前锁未被任何线程持有 + if (compareAndSetState(0, acquires)) { //以cas方式获取锁 + setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程 + return true;//获取锁成功,非重入 + } + } + else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了 + int nextc = c + acquires;//计算state变量要更新的值 + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc);//非同步方式更新state值 + return true; //获取锁成功,重入 + } + return false; //走到这里说明尝试获取锁失败 +} +``` + +这是非公平模式下获取锁的通用方法。它囊括了当前线程在尝试获取锁时的所有可能情况: + +- 1.当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,然后返回成功的结果;若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。 +- 2.若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。 +- 3.若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果 + +因为我们用state来统计锁被线程重入的次数,所以当前线程尝试获取锁的操作是否成功可以简化为:state值是否成功累加1,是则尝试获取锁成功,否则尝试获取锁失败。 + +其实这里还可以思考一个问题:nonfairTryAcquire已经实现了一个囊括所有可能情况的尝试获取锁的方式,为何在刚进入lock方法时还要通过compareAndSetState(0, 1)去获取锁,毕竟后者只有在锁未被任何线程持有时才能执行成功,我们完全可以把compareAndSetState(0, 1)去掉,对最后的结果不会有任何影响。这种在进行通用逻辑处理之前针对某些特殊情况提前进行处理的方式在后面还会看到,一个直观的想法就是它能提升性能,而代价是牺牲一定的代码简洁性。 + +退回到上层的acquire方法, + +``` +public final void acquire(int arg) { + if (!tryAcquire(arg) && //当前线程尝试获取锁,若获取成功返回true,否则false + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //只有当前线程获取锁失败才会执行者这部分代码 + selfInterrupt(); +} +``` + +tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看addWaiter(Node.EXCLUSIVE) +部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。这部分代码可以说是整个加锁流程源码的精华,充分体现了并发编程的艺术性。 + +### 3.3 获取锁失败的线程如何安全的加入同步队列:addWaiter() + +这部分逻辑在addWaiter()方法中 + +``` +/** + * 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);//首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null + // 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(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分“重复代码”和尝试获取锁时的“重复代码”一样,对某些特殊情况 +进行提前处理,牺牲一定的代码可读性换取性能提升。 + +``` +/** + * Inserts node into queue, initializing if necessary. See picture above. + * @param node the node to insert + * @return node's predecessor + */ +private Node enq(final Node node) { + for (;;) { + Node t = tail;//t指向当前队列的最后一个节点,队列为空则为null + if (t == null) { // Must initialize //队列为空 + if (compareAndSetHead(new Node())) //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功 + tail = head;//尾指针指向首结点 + } else { //队列不为空 + node.prev = t; + if (compareAndSetTail(t, node)) { //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功 + t.next = node; //原尾结点的next指针指向当前结点 + return t; + } + } + } +} +``` + +这里有两个CAS操作: + +- compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功 + +``` +/** + * CAS head field. Used only by enq. + */ +private final boolean compareAndSetHead(Node update) { + return unsafe.compareAndSwapObject(this, headOffset, null, update); +} +``` + +- compareAndSetTail(t, node),CAS方式更新tial指针,仅当原值为t时更新成功 + +``` +/** + * CAS tail field. Used only by enq. + */ +private final boolean compareAndSetTail(Node expect, Node update) { + return unsafe.compareAndSwapObject(this, tailOffset, expect, update); +} +``` + +外层的for循环保证了所有获取锁失败的线程经过失败重试后最后都能加入同步队列。因为AQS的同步队列是不带哨兵结点的,故当队列为空时要进行特殊处理,这部分在if分句中。注意当前线程所在的结点不能直接插入 +空队列,因为阻塞的线程是由前驱结点进行唤醒的。故先要插入一个结点作为队列首元素,当锁释放时由它来唤醒后面被阻塞的线程,从逻辑上这个队列首元素也可以表示当前正获取锁的线程,虽然并不一定真实持有其线程实例。 + +首先通过new Node()创建一个空结点,然后以CAS方式让头指针指向该结点(该结点并非当前线程所在的结点),若该操作成功,则将尾指针也指向该结点。这部分的操作流程可以用下图表示 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805183957849-785041089.png) + +当队列不为空,则执行通用的入队逻辑,这部分在else分句中 + +``` +else { + node.prev = t;//step1:待插入结点pre指针指向原尾结点 + if (compareAndSetTail(t, node)) { step2:CAS方式更改尾指针 + t.next = node; //原尾结点next指针指向新的结点 + return t; + } + } +``` + +首先当前线程所在的结点的前向指针pre指向当前线程认为的尾结点,源码中用t表示。然后以CAS的方式将尾指针指向当前结点,该操作仅当tail=t,即尾指针在进行CAS前未改变时成功。若CAS执行成功,则将原尾结点的后向指针next指向新的尾结点。整个过程如下图所示 + +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184016249-998899089.png) + +整个入队的过程并不复杂,是典型的CAS加失败重试的乐观锁策略。其中只有更新头指针和更新尾指针这两步进行了CAS同步,可以预见高并发场景下性能是非常好的。但是本着质疑精神我们不禁会思考下这么做真的线程安全吗? +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184052040-1494856465.png) + +- 1.队列为空的情况: + 因为队列为空,故head=tail=null,假设线程执行2成功,则在其执行3之前,因为tail=null,其他进入该方法的线程因为head不为null将在2处不停的失败,所以3即使没有同步也不会有线程安全问题。 +- 2.队列不为空的情况: + 假设线程执行5成功,则此时4的操作必然也是正确的(当前结点的prev指针确实指向了队列尾结点,换句话说tail指针没有改变,如若不然5必然执行失败),又因为4执行成功,当前节点在队列中的次序已经确定了,所以6何时执行对线程安全不会有任何影响,比如下面这种情况 + +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184134587-1898400226.png) + +为了确保真的理解了它,可以思考这个问题:把enq方法图中的4放到5之后,整个入队的过程还线程安全吗? + +到这为止,获取锁失败的线程加入同步队列的逻辑就结束了。但是线程加入同步队列后会做什么我们并不清楚,这部分在acquireQueued方法中 + +### 3.4 线程加入同步队列后会做什么:acquireQueued() + +先看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分句 + if (p == head && tryAcquire(arg)) { + setHead(node); //将当前结点设置为队列头结点 + p.next = null; // help GC + failed = false; + return interrupted;//正常情况下死循环唯一的出口 + } + //第二个if分句 + if (shouldParkAfterFailedAcquire(p, node) && //判断是否要阻塞当前线程 + parkAndCheckInterrupt()) //阻塞当前线程 + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} +``` + +这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成。第一个if分句中,当前线程首先会判断前驱结点是否是头结点,如果是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。为什么必须前驱结点为头结点才尝试去获取锁?因为头结点表示当前正占有锁的线程,正常情况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是我们希望看到的。然而还有一种情况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不应该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,然后将自身连接在它后面。一旦我们成功获取了锁并成功将自身设置为头结点,就会跳出for循环。否则就会执行第二个if分句:确保前驱结点的状态为SIGNAL,然后阻塞当前线程。 + +先来看shouldParkAfterFailedAcquire(p, 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; + if (ws == Node.SIGNAL) //状态为SIGNAL + + /* + * This node has already set status asking a release + * to signal it, so it can safely park. + */ + return true; + if (ws > 0) { //状态为CANCELLED, + /* + * Predecessor was cancelled. Skip over predecessors and + * indicate retry. + */ + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { //状态为初始化状态(ReentrentLock语境下) + /* + * 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; +} +``` + +可以看到针对前驱结点pred的状态会进行不同的处理 + +- 1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程。 +- 2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。 +- 3.pred的状态为初始化状态,此时通过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改为SIGNAL。 + +其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。 + +但是要注意只有在前驱结点已经是SIGNAL状态后才会执行后面的方法立即阻塞,对应上面的第一种情况。其他两种情况则因为返回false而重新执行一遍 +for循环。这种延迟阻塞其实也是一种高并发场景下的优化,试想我如果在重新执行循环的时候成功获取了锁,是不是线程阻塞唤醒的开销就省了呢? + +最后我们来看看阻塞线程的方法parkAndCheckInterrupt + +shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程,源码如下: + +``` +/** + * Convenience method to park and then check if interrupted + * + * @return {@code true} if interrupted + */ +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +``` + +该方法内部通过调用LockSupport的park方法来阻塞当前线程,不清楚LockSupport的可以看看这里。[LockSupport功能简介及原理浅析](https://www.cnblogs.com/takumicx/p/9328459.html) + +下面通过一张流程图来说明线程从加入同步队列到成功获取锁的过程 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184242438-1630878927.png) + +概括的说,线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部。 + +### 3.5 加锁流程源码总结 + +ReentrantLock非公平模式下的加锁流程如下 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184331148-1572746602.png) + +## 4.非公平模式解锁流程 + +### 4.1 解锁流程源码解读 + +解锁的源码相对简单,源码如下: + +``` +public void unlock() { + sync.release(1); +} +public final boolean release(int arg) { + if (tryRelease(arg)) { //释放锁(state-1),若释放后锁可被其他线程获取(state=0),返回true + Node h = head; + //当前队列不为空且头结点状态不为初始化状态(0) + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); //唤醒同步队列中被阻塞的线程 + return true; + } + return false; +} +``` + +正确找到sync的实现类,找到真正的入口方法,主要内容都在一个if语句中,先看下判断条件tryRelease方法 + +``` +protected final boolean tryRelease(int releases) { + int c = getState() - releases; //计算待更新的state值 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { //待更新的state值为0,说明持有锁的线程未重入,一旦释放锁其他线程将能获取 + free = true; + setExclusiveOwnerThread(null);//清除锁的持有线程标记 + } + setState(c);//更新state值 + return free; +} +``` + +tryRelease其实只是将线程持有锁的次数减1,即将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false。由于执行该方法的线程必然持有锁,故该方法不需要任何同步操作。 +若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程。不过在此之前需要进行两个条件的判断: + +- h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作 +- h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。 + +接下来就是唤醒线程的操作,unparkSuccessor(h)源码如下 + +``` +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); + + /* + * Thread to unpark is held in successor, which is normally + * just the next node. But if cancelled or apparently null, + * traverse backwards from tail to find the actual + * non-cancelled successor. + */ + 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); +} +``` + +一般情况下只要唤醒后继结点的线程就行了,但是后继结点可能已经取消等待,所以从队列尾部往前回溯,找到离头结点最近的正常结点,并唤醒其线程。 + +### 4.2 解锁流程源码总结 + +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805184348967-1221850147.png) + +## 5. 公平锁相比非公平锁的不同 + +公平锁模式下,对锁的获取有严格的条件限制。在同步队列有线程等待的情况下,所有线程在获取锁前必须先加入同步队列。队列中的线程按加入队列的先后次序获得锁。 +从公平锁加锁的入口开始, +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805185314873-766370811.png) + +对比非公平锁,少了非重入式获取锁的方法,这是第一个不同点 + +接着看获取锁的通用方法tryAcquire(),该方法在线程未进入队列,加入队列阻塞前和阻塞后被唤醒时都会执行。 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805185331385-713414476.png) + +在真正CAS获取锁之前加了判断,内容如下 + +``` +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()); +} +``` + +从方法名我们就可知道这是判断队列中是否有优先级更高的等待线程,队列中哪个线程优先级最高?由于头结点是当前获取锁的线程,队列中的第二个结点代表的线程优先级最高。 +那么我们只要判断队列中第二个结点是否存在以及这个结点是否代表当前线程就行了。这里分了两种情况进行探讨: + +1. 第二个结点已经完全插入,但是这个结点是否就是当前线程所在结点还未知,所以通过s.thread != Thread.currentThread()进行判断,如果为true,说明第二个结点代表其他线程。 +2. 第二个结点并未完全插入,我们知道结点入队一共分三步: + +- 1.待插入结点的pre指针指向原尾结点 +- 2.CAS更新尾指针 +- 3.原尾结点的next指针指向新插入结点 + +所以(s = h.next) == null 就是用来判断2刚执行成功但还未执行3这种情况的。这种情况第二个结点必然属于其他线程。 +以上两种情况都会使该方法返回true,即当前有优先级更高的线程在队列中等待,那么当前线程将不会执行CAS操作去获取锁,保证了线程获取锁的顺序与加入同步队列的顺序一致,很好的保证了公平性,但也增加了获取锁的成本。 + +## 6. 一些疑问的解答 + +### 6.1 为什么基于FIFO的同步队列可以实现非公平锁? + +由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的得到锁。从这个意义上,对于在同步队列中等待的线程而言,它们获得锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。然而,线程并非只有在加入队列后才有机会获得锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。回看下非公平锁的加锁流程,线程在进入同步队列等待之前有两次抢占锁的机会: + +- 第一次是非重入式的获取锁,只有在当前锁未被任何线程占有(包括自身)时才能成功; +- 第二次是在进入同步队列前,包含所有情况的获取锁的方式。 + +只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待。而线程释放锁时是先释放锁(修改state值),然后才唤醒后继结点的线程的。试想下这种情况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。整个过程如下图所示 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805185020232-615609336.png) + +如果以线程第一次尝试获取锁到最后成功获取锁的次序来看,非公平锁确实很不公平。因为在队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待其他线程唤醒,在获取锁之前还要检查前驱结点是否为头结点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁。这也就非公平在高并发情况下会出现的饥饿问题。那我们再开发中为什么大多使用会导致饥饿的非公平锁?很简单,因为它性能好啊。 + +### 6.2 为什么非公平锁性能好 + +非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这种好处体现在两个方面: + +- 1.线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。 +- 2.减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。除此之外,加锁流程中至少有两处通过将某些特殊情况提前来减少CAS操作的竞争,增加并发情况下的性能。一处就是获取锁时将非重入的情况提前,如下图所示 + ![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805185241957-1365580718.png) + +另一处就是入队的操作,将同步队列非空的情况提前处理 +![img](https://images2018.cnblogs.com/blog/1422237/201808/1422237-20180805185255573-1887096699.png) + +这两部分的代码在之后的通用逻辑处理中都有,很显然属于重复代码,但因为避免了执行无意义的流程代码,比如for循环,获取同步状态等,高并发场景下也能减少CAS竞争失败的可能。 + +## 7. 阅读源码的收获 + +- 1.熟悉了ReentrantLock的内部构造以及加锁和解锁的流程,理解了非公平锁和公平锁实现的本质区别以及为何前者相比后者有更好的性能。以此为基础,我们可以更好的使用ReentrantLock。 +- 2.通过对部分实现细节的学习,了解了如何以CAS算法构建无锁的同步队列,我们可以借鉴并以此来构建自己的无锁的并发容器。 \ No newline at end of file diff --git a/week_03/51/Semaphore_51.md b/week_03/51/Semaphore_51.md new file mode 100644 index 0000000..90babfe --- /dev/null +++ b/week_03/51/Semaphore_51.md @@ -0,0 +1,250 @@ +## 类介绍 + +Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。比如控制用户的访问量,同一时刻只允许1000个用户同时使用系统,如果超过1000个并发,则需要等待。 + +## 使用场景 + +比如模拟一个停车场停车信号,假设停车场只有两个车位,一开始两个车位都是空的。这时如果同时来了两辆车,看门人允许它们进入停车场,然后放下车拦。以后来的车必须在入口等待,直到停车场中有车辆离开。这时,如果有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开一辆,则又可以放入一辆,如此往复。 + + + +```java +public class SemaphoreDemo { + private static Semaphore s = new Semaphore(2); + public static void main(String[] args) { + ExecutorService pool = Executors.newCachedThreadPool(); + pool.submit(new ParkTask("1")); + pool.submit(new ParkTask("2")); + pool.submit(new ParkTask("3")); + pool.submit(new ParkTask("4")); + pool.submit(new ParkTask("5")); + pool.submit(new ParkTask("6")); + pool.shutdown(); + } + + static class ParkTask implements Runnable { + private String name; + public ParkTask(String name) { + this.name = name; + } + @Override + public void run() { + try { + s.acquire(); + System.out.println("Thread "+this.name+" start..."); + TimeUnit.SECONDS.sleep(new Random().nextInt(10)); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + s.release(); + } + } + } +} +``` + +## Semaphore 源码分析 + +Semaphore 通过使用内部类Sync继承AQS来实现。 + 支持公平锁和非公平锁。内部使用的AQS的共享锁。 + 具体实现可参考 [AbstractQueuedSynchronizer 源码分析](https://www.jianshu.com/p/dbe18cea28e7) + +Semaphore 的结构如下: + + + +![img](https:////upload-images.jianshu.io/upload_images/2843224-c7695d1692a9a0d7.png?imageMogr2/auto-orient/strip|imageView2/2/w/813/format/webp) + +Semaphore 类结构 + +#### Semaphore构造 + + + +```java +public Semaphore(int permits) { + sync = new NonfairSync(permits); +} + +public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); +} +``` + +构造方法指定信号量的许可数量,默认采用的是非公平锁,也只可以指定为公平锁。 + permits赋值给AQS中的state变量。 + +#### acquire:可响应中断的获得信号量 + + + +```java +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} + +public void acquire(int permits) throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireSharedInterruptibly(permits); +} +``` + +获得信号量方法,这两个方法支持 Interrupt中断机制,可使用acquire() 方法每次获取一个信号量,也可以使用acquire(int permits) 方法获取指定数量的信号量 。 + +#### acquire:不可响应中断的获取信号量 + + + +```java +public void acquireUninterruptibly() { + sync.acquireShared(1); +} + +public void acquireUninterruptibly(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireShared(permits); +} +``` + +这两个方法不响应Interrupt中断机制,其它功能同acquire方法机制。 + +#### tryAcquire 方法,尝试获得信号量 + + + +```java +public boolean tryAcquire() { + return sync.nonfairTryAcquireShared(1) >= 0; +} + +public boolean tryAcquire(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); +} + +public boolean tryAcquire(int permits, long timeout, TimeUnit unit) + throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout)); +} +``` + +尝试获得信号量有三个方法。 + +1. 尝试获取信号量,如果获取成功则返回true,否则马上返回false,不会阻塞当前线程。 +2. 尝试获取信号量,如果在指定的时间内获得信号量,则返回true,否则返回false +3. 尝试获取指定数量的信号量,如果在指定的时间内获得信号量,则返回true,否则返回false。 + +#### release 释放信号量 + + + +```java +public void release() { + sync.releaseShared(1); +} +``` + +调用AQS中的releaseShared方法,使得state每次减一来控制信号量。 + +#### availablePermits方法,获取当前剩余的信号量数量 + + + +```java +public int availablePermits() { + return sync.getPermits(); +} + +//=========Sync类======== +final int getPermits() { + return getState(); + } +``` + +该方法返回AQS中state变量的值,当前剩余的信号量个数 + +#### drainPermits方法 + + + +```java +public int drainPermits() { + return sync.drainPermits(); +} + +//=========Sync类======== +final int drainPermits() { + for (;;) { + int current = getState(); + if (current == 0 || compareAndSetState(current, 0)) + return current; + } +} +``` + +获取并返回立即可用的所有许可。Sync类的drainPermits方法,获取1个信号量后将可用的信号量个数置为0。例如总共有10个信号量,已经使用了5个,再调用drainPermits方法后,可以获得一个信号量,剩余4个信号量就消失了,总共可用的信号量就变成6个了。 + +#### reducePermits 方法 + + + +```java +protected void reducePermits(int reduction) { + if (reduction < 0) throw new IllegalArgumentException(); + sync.reducePermits(reduction); +} + +//=========Sync类======== +final void reducePermits(int reductions) { + for (;;) { + int current = getState(); + int next = current - reductions; + if (next > current) // underflow + throw new Error("Permit count underflow"); + if (compareAndSetState(current, next)) + return; + } +} +``` + +该方法是protected 方法,减少信号量个数 + +#### 判断AQS等待队列中是否还有Node + + + +```java +public final boolean hasQueuedThreads() { + return sync.hasQueuedThreads(); +} + +//=========AbstractQueuedSynchronizer类======== +public final boolean hasQueuedThreads() { + //头结点不等于尾节点就说明链表中还有元素 + return head != tail; +} +``` + +#### getQueuedThreads方法 + + + +```java +protected Collection getQueuedThreads() { + return sync.getQueuedThreads(); +} + +//=========AbstractQueuedSynchronizer类======== +public final Collection getQueuedThreads() { + ArrayList list = new ArrayList(); + for (Node p = tail; p != null; p = p.prev) { + Thread t = p.thread; + if (t != null) + list.add(t); + } + return list; +} +``` + +该方法获取AQS中等待队列中所有未获取信号量的线程相关的信息(等待获取信号量的线程相关信息)。 \ No newline at end of file diff --git a/week_03/51/Semaphore_51_1.md b/week_03/51/Semaphore_51_1.md new file mode 100644 index 0000000..a0b697d --- /dev/null +++ b/week_03/51/Semaphore_51_1.md @@ -0,0 +1,386 @@ +## 问题 + +(1)Semaphore是什么? + +(2)Semaphore具有哪些特性? + +(3)Semaphore通常使用在什么场景中? + +(4)Semaphore的许可次数是否可以动态增减? + +(5)Semaphore如何实现限流? + +## 简介 + +Semaphore,信号量,它保存了一系列的许可(permits),每次调用acquire()都将消耗一个许可,每次调用release()都将归还一个许可。 + +## 特性 + +Semaphore通常用于限制同一时间对共享资源的访问次数上,也就是常说的限流。 + +下面我们一起来学习Java中Semaphore是如何实现的。 + +## 类结构 + +![Semaphore](https://gitee.com/alan-tang-tt/yuan/raw/master/%E6%AD%BB%E7%A3%95%20java%E5%90%8C%E6%AD%A5%E7%B3%BB%E5%88%97/resource/Semaphore.png) + +Semaphore中包含了一个实现了AQS的同步器Sync,以及它的两个子类FairSync和NonFairSync,这说明Semaphore也是区分公平模式和非公平模式的。 + +## 源码分析 + +基于之前对于ReentrantLock和ReentrantReadWriteLock的分析,这篇文章相对来说比较简单,之前讲过的一些方法将直接略过,有兴趣的可以拉到文章底部查看之前的文章。 + +### 内部类Sync + +``` +// java.util.concurrent.Semaphore.Sync +abstract static class Sync extends AbstractQueuedSynchronizer { + private static final long serialVersionUID = 1192457210091910933L; + // 构造方法,传入许可次数,放入state中 + Sync(int permits) { + setState(permits); + } + // 获取许可次数 + final int getPermits() { + return getState(); + } + // 非公平模式尝试获取许可 + final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 看看还有几个许可 + int available = getState(); + // 减去这次需要获取的许可还剩下几个许可 + int remaining = available - acquires; + // 如果剩余许可小于0了则直接返回 + // 如果剩余许可不小于0,则尝试原子更新state的值,成功了返回剩余许可 + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } + } + // 释放许可 + protected final boolean tryReleaseShared(int releases) { + for (;;) { + // 看看还有几个许可 + int current = getState(); + // 加上这次释放的许可 + int next = current + releases; + // 检测溢出 + if (next < current) // overflow + throw new Error("Maximum permit count exceeded"); + // 如果原子更新state的值成功,就说明释放许可成功,则返回true + if (compareAndSetState(current, next)) + return true; + } + } + // 减少许可 + final void reducePermits(int reductions) { + for (;;) { + // 看看还有几个许可 + int current = getState(); + // 减去将要减少的许可 + int next = current - reductions; + // 检测举出 + if (next > current) // underflow + throw new Error("Permit count underflow"); + // 原子更新state的值,成功了返回true + if (compareAndSetState(current, next)) + return; + } + } + // 销毁许可 + final int drainPermits() { + for (;;) { + // 看看还有几个许可 + int current = getState(); + // 如果为0,直接返回 + // 如果不为0,把state原子更新为0 + if (current == 0 || compareAndSetState(current, 0)) + return current; + } + } +} +``` + +通过Sync的几个实现方法,我们获取到以下几点信息: + +(1)许可是在构造方法时传入的; + +(2)许可存放在状态变量state中; + +(3)尝试获取一个许可的时候,则state的值减1; + +(4)当state的值为0的时候,则无法再获取许可; + +(5)释放一个许可的时候,则state的值加1; + +(6)许可的个数可以动态改变; + +### 内部类NonfairSync + +``` +// java.util.concurrent.Semaphore.NonfairSync +static final class NonfairSync extends Sync { + private static final long serialVersionUID = -2694183684443567898L; + // 构造方法,调用父类的构造方法 + NonfairSync(int permits) { + super(permits); + } + // 尝试获取许可,调用父类的nonfairTryAcquireShared()方法 + protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); + } +} +``` + +非公平模式下,直接调用父类的nonfairTryAcquireShared()尝试获取许可。 + +### 内部类FairSync + +``` +// java.util.concurrent.Semaphore.FairSync +static final class FairSync extends Sync { + private static final long serialVersionUID = 2014338818796000944L; + // 构造方法,调用父类的构造方法 + FairSync(int permits) { + super(permits); + } + // 尝试获取许可 + protected int tryAcquireShared(int acquires) { + for (;;) { + // 公平模式需要检测是否前面有排队的 + // 如果有排队的直接返回失败 + if (hasQueuedPredecessors()) + return -1; + // 没有排队的再尝试更新state的值 + int available = getState(); + int remaining = available - acquires; + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } + } +} +``` + +公平模式下,先检测前面是否有排队的,如果有排队的则获取许可失败,进入队列排队,否则尝试原子更新state的值。 + +### 构造方法 + +``` +// 构造方法,创建时要传入许可次数,默认使用非公平模式 +public Semaphore(int permits) { + sync = new NonfairSync(permits); +} +// 构造方法,需要传入许可次数,及是否公平模式 +public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); +} +``` + +创建Semaphore时需要传入许可次数。 + +Semaphore默认也是非公平模式,但是你可以调用第二个构造方法声明其为公平模式。 + +下面的方法在学习过前面的内容看来都比较简单,彤哥这里只列举Semaphore支持的一些功能了。 + +以下的方法都是针对非公平模式来描述。 + +### acquire()方法 + +``` +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +``` + +获取一个许可,默认使用的是可中断方式,如果尝试获取许可失败,会进入AQS的队列中排队。 + +### acquireUninterruptibly()方法 + +``` +public void acquireUninterruptibly() { + sync.acquireShared(1); +} +``` + +获取一个许可,非中断方式,如果尝试获取许可失败,会进入AQS的队列中排队。 + +### tryAcquire()方法 + +``` +public boolean tryAcquire() { + return sync.nonfairTryAcquireShared(1) >= 0; +} +``` + +尝试获取一个许可,使用Sync的非公平模式尝试获取许可方法,不论是否获取到许可都返回,只尝试一次,不会进入队列排队。 + +### tryAcquire(long timeout, TimeUnit unit)方法 + +``` +public boolean tryAcquire(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); +} +``` + +尝试获取一个许可,先尝试一次获取许可,如果失败则会等待timeout时间,这段时间内都没有获取到许可,则返回false,否则返回true; + +### release()方法 + +``` +public void release() { + sync.releaseShared(1); +} +``` + +释放一个许可,释放一个许可时state的值会加1,并且会唤醒下一个等待获取许可的线程。 + +### acquire(int permits)方法 + +``` +public void acquire(int permits) throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireSharedInterruptibly(permits); +} +``` + +一次获取多个许可,可中断方式。 + +### acquireUninterruptibly(int permits)方法 + +``` +public void acquireUninterruptibly(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireShared(permits); +} +``` + +一次获取多个许可,非中断方式。 + +### tryAcquire(int permits)方法 + +``` +public boolean tryAcquire(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + return sync.nonfairTryAcquireShared(permits) >= 0; +} +``` + +一次尝试获取多个许可,只尝试一次。 + +### tryAcquire(int permits, long timeout, TimeUnit unit)方法 + +``` +public boolean tryAcquire(int permits, long timeout, TimeUnit unit) + throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout)); +} +``` + +尝试获取多个许可,并会等待timeout时间,这段时间没获取到许可则返回false,否则返回true。 + +### release(int permits)方法 + +``` +public void release(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + sync.releaseShared(permits); +} +``` + +一次释放多个许可,state的值会相应增加permits的数量。 + +### availablePermits()方法 + +``` +public int availablePermits() { + return sync.getPermits(); +} +``` + +获取可用的许可次数。 + +### drainPermits()方法 + +``` +public int drainPermits() { + return sync.drainPermits(); +} +``` + +销毁当前可用的许可次数,对于已经获取的许可没有影响,会把当前剩余的许可全部销毁。 + +### reducePermits(int reduction)方法 + +``` +protected void reducePermits(int reduction) { + if (reduction < 0) throw new IllegalArgumentException(); + sync.reducePermits(reduction); +} +``` + +减少许可的次数。 + +## 总结 + +(1)Semaphore,也叫信号量,通常用于控制同一时刻对共享资源的访问上,也就是限流场景; + +(2)Semaphore的内部实现是基于AQS的共享锁来实现的; + +(3)Semaphore初始化的时候需要指定许可的次数,许可的次数是存储在state中; + +(4)获取一个许可时,则state值减1; + +(5)释放一个许可时,则state值加1; + +(6)可以动态减少n个许可; + +(7)可以动态增加n个许可吗? + +## 彩蛋 + +(1)如何动态增加n个许可? + +答:调用release(int permits)即可。我们知道释放许可的时候state的值会相应增加,再回头看看释放许可的源码,发现与ReentrantLock的释放锁还是有点区别的,Semaphore释放许可的时候并不会检查当前线程有没有获取过许可,所以可以调用释放许可的方法动态增加一些许可。 + +(2)如何实现限流? + +答:限流,即在流量突然增大的时候,上层要能够限制住突然的大流量对下游服务的冲击,在分布式系统中限流一般做在网关层,当然在个别功能中也可以自己简单地来限流,比如秒杀场景,假如只有10个商品需要秒杀,那么,服务本身可以限制同时只进来100个请求,其它请求全部作废,这样服务的压力也不会太大。 + +使用Semaphore就可以直接针对这个功能来限流,以下是代码实现: + +```java +public class SemaphoreTest { + public static final Semaphore SEMAPHORE = new Semaphore(100); + public static final AtomicInteger failCount = new AtomicInteger(0); + public static final AtomicInteger successCount = new AtomicInteger(0); + + public static void main(String[] args) { + for (int i = 0; i < 1000; i++) { + new Thread(()->seckill()).start(); + } + } + + public static boolean seckill() { + if (!SEMAPHORE.tryAcquire()) { + System.out.println("no permits, count="+failCount.incrementAndGet()); + return false; + } + + try { + // 处理业务逻辑 + Thread.sleep(2000); + System.out.println("seckill success, count="+successCount.incrementAndGet()); + } catch (InterruptedException e) { + // todo 处理异常 + e.printStackTrace(); + } finally { + SEMAPHORE.release(); + } + return true; + } +} +``` \ No newline at end of file diff --git a/week_03/51/synchronized_51.md b/week_03/51/synchronized_51.md new file mode 100644 index 0000000..22b626e --- /dev/null +++ b/week_03/51/synchronized_51.md @@ -0,0 +1,912 @@ +# 内容导航 + +1. 从synchronized的字节码说起 +2. 什么是monitor +3. 分析synchronized的源码 + +# 从synchronized的字节码说起 + +由于synchronized的实现是在jvm层面,所以我们如果要看它的源码,需要从字节码入手。这段代码演示了synchronized作为实例锁的两种用法,我们观察一下这段代码生成的字节码 + + + +```java + public class App +{ + public synchronized void test1(){ + } + public void test2(){ + synchronized (this){ + + } + } + public static void main( String[] args ){ + System.out.println( "Hello World!" ); + } +} +``` + +> 进入classpath目录下找到App.class文件, 在cmd中输入 javap -v App.class查看字节码 + + + +```cmd +public synchronized void test1(); + descriptor: ()V + flags: ACC_PUBLIC, ACC_SYNCHRONIZED + Code: + stack=0, locals=1, args_size=1 + 0: return + LineNumberTable: + line 10: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 1 0 this Lcom/gupaoedu/openclass/App; + + public void test2(); + descriptor: ()V + flags: ACC_PUBLIC + Code: + stack=2, locals=3, args_size=1 + 0: aload_0 + 1: dup + 2: astore_1 + 3: monitorenter //监视器进入,获取锁 + 4: aload_1 + 5: monitorexit //监视器退出,释放锁 + 6: goto 14 + 9: astore_2 + 10: aload_1 + 11: monitorexit + 12: aload_2 + 13: athrow + 14: return +``` + +通过字节码我们可以发现,修饰在方法层面的同步关键字,会多一个 ACC_SYNCHRONIZED的flag;修饰在代码块层面的同步块会多一个 monitorenter和 monitorexit关键字。无论采用哪一种方式,本质上都是对一个对象的监视器(monitor)进行获取,而这个获取的过程是排他的,也就是同一个时刻只能有一个线程获得同步块对象的监视器。 + 在 [synchronized的原理分析](https://segmentfault.com/a/1190000017255044)这篇文章中,有提到对象监视器。 + +> synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。当我们的JVM把字节码加载到内存的时候,会对这两个指令进行解析。这两个字节码都需要一个Object类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那么这个对象就是加锁和解锁的对象;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,获取对应的对象实例或Class对象来作为锁对象 + +# 什么是monitor + +在分析源代码之前需要了解oop, oopDesc, markOop等相关概念,在[Synchronized的原理分析](https://segmentfault.com/a/1190000017255044)这篇文章中,我们讲到了synchronized的同步锁实际上是存储在对象头中,这个对象头是一个Java对象在内存中的布局的一部分。Java中的每一个Object在JVM内部都会有一个native的C++对象oop/oopDesc与之对应。在hotspot源码 oop.hpp中oopDesc的定义如下 + + + +```c +class oopDesc { + friend class VMStructs; + private: + volatile markOop _mark; + union _metadata { + Klass* _klass; + narrowKlass _compressed_klass; + } _metadata; +``` + +其中 markOop就是我们所说的Mark Word,用于存储锁的标识。 + hotspot源码 markOop.hpp文件代码片段 + + + +```c +class markOopDesc: public oopDesc { + private: + // Conversion + uintptr_t value() const { return (uintptr_t) this; } + + public: + // Constants + enum { age_bits = 4, + lock_bits = 2, + biased_lock_bits = 1, + max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, + hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, + cms_bits = LP64_ONLY(1) NOT_LP64(0), + epoch_bits = 2 + }; + ... +} +``` + +markOopDesc继承自oopDesc,并且扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象,在hotspot虚拟机中,采用ObjectMonitor类来实现monitor + + + +```c +bool has_monitor() const { + return ((value() & monitor_value) != 0); + } + ObjectMonitor* monitor() const { + assert(has_monitor(), "check"); + // Use xor instead of &~ to provide one extra tag-bit check. + return (ObjectMonitor*) (value() ^ monitor_value); + } +``` + +在 ObjectMonitor.hpp中,可以看到ObjectMonitor的定义 + + + +```c + class ObjectMonitor { +... + ObjectMonitor() { + _header = NULL; //markOop对象头 + _count = 0; + _waiters = 0, //等待线程数 + _recursions = 0; //重入次数 + _object = NULL; + _owner = NULL; //获得ObjectMonitor对象的线程 + _WaitSet = NULL; //处于wait状态的线程,会被加入到waitSet + _WaitSetLock = 0 ; + _Responsible = NULL ; + _succ = NULL ; + _cxq = NULL ; + FreeNext = NULL ; + _EntryList = NULL ; //处于等待锁BLOCKED状态的线程 + _SpinFreq = 0 ; + _SpinClock = 0 ; + OwnerIsThread = 0 ; + _previous_owner_tid = 0; //监视器前一个拥有线程的ID + } +... +``` + +简单总结一下,同步块的实现使用 monitorenter和 monitorexit指令,而同步方法是依靠方法修饰符上的flag ACC_SYNCHRONIZED来完成。其本质是对一个对象监视器(monitor)进行获取,这个获取过程是排他的,也就是同一个时刻只能有一个线程获得由synchronized所保护对象的监视器。所谓的监视器,实际上可以理解为一个同步工具,它是由Java对象进行描述的。在Hotspot中,是通过ObjectMonitor来实现,每个对象中都会内置一个ObjectMonitor对象 + +![img](https:////upload-images.jianshu.io/upload_images/5303047-3f8a2b8738ca85a1.png?imageMogr2/auto-orient/strip|imageView2/2/w/929/format/webp) + +synchroinzed简图 + +# 简单分析synchronized的源码 + +从 monitorenter和 monitorexit这两个指令来开始阅读源码,JVM将字节码加载到内存以后,会对这两个指令进行解释执行, monitorenter, monitorexit的指令解析是通过 InterpreterRuntime.cpp中的两个方法实现 + + + +```c +InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem) +InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem) +//JavaThread 当前获取锁的线程 +//BasicObjectLock 基础对象锁 +``` + +我们基于monitorenter为入口,沿着偏向锁->轻量级锁->重量级锁的路径来分析synchronized的实现过程 + + + +```c +IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) +#ifdef ASSERT + thread->last_frame().interpreter_frame_verify_monitor(elem); +#endif + ... + if (UseBiasedLocking) { + // Retry fast entry if bias is revoked to avoid unnecessary inflation + ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); + } else { + ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); + } + ... +#ifdef ASSERT + thread->last_frame().interpreter_frame_verify_monitor(elem); +#endif +IRT_END +``` + +UseBiasedLocking是在JVM启动的时候,是否启动偏向锁的标识 + +1. 如果支持偏向锁,则执行 ObjectSynchronizer::fast_enter的逻辑 +2. 如果不支持偏向锁,则执行 ObjectSynchronizer::slow_enter逻辑,绕过偏向锁,直接进入轻量级锁 + +ObjectSynchronizer::fast_enter的实现在 synchronizer.cpp文件中,代码如下 + + + +```c +void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { + if (UseBiasedLocking) { //判断是否开启了偏向锁 + if (!SafepointSynchronize::is_at_safepoint()) { //如果不处于全局安全点 + //通过`revoke_and_rebias`这个函数尝试获取偏向锁 + BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); + if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {//如果是撤销与重偏向直接返回 + return; + } + } else {//如果在安全点,撤销偏向锁 + assert(!attempt_rebias, "can not rebias toward VM thread"); + BiasedLocking::revoke_at_safepoint(obj); + } + assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); + } + + slow_enter (obj, lock, THREAD) ; +} +``` + +fast_enter方法的主要流程做一个简单的解释 + +1. 再次检查偏向锁是否开启 +2. 当处于不安全点时,通过 revoke_and_rebias尝试获取偏向锁,如果成功则直接返回,如果失败则进入轻量级锁获取过程 +3. revoke_and_rebias这个偏向锁的获取逻辑在 biasedLocking.cpp中 +4. 如果偏向锁未开启,则进入 slow_enter获取轻量级锁的流程 + +## 偏向锁的获取逻辑 + +BiasedLocking::revoke_and_rebias 是用来获取当前偏向锁的状态(可能是偏向锁撤销后重新偏向)。这个方法的逻辑在 biasedLocking.cpp中 + + + +```c +BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) { + assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint"); + markOop mark = obj->mark(); //获取锁对象的对象头 + //判断mark是否为可偏向状态,即mark的偏向锁标志位为1,锁标志位为 01,线程id为null + if (mark->is_biased_anonymously() && !attempt_rebias) { + //这个分支是进行对象的hashCode计算时会进入,在一个非全局安全点进行偏向锁撤销 + markOop biased_value = mark; + //创建一个非偏向的markword + markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); + //Atomic:cmpxchg_ptr是CAS操作,通过cas重新设置偏向锁状态 + markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark); + if (res_mark == biased_value) {//如果CAS成功,返回偏向锁撤销状态 + return BIAS_REVOKED; + } + } else if (mark->has_bias_pattern()) {//如果锁对象为可偏向状态(biased_lock:1, lock:01,不管线程id是否为空),尝试重新偏向 + Klass* k = obj->klass(); + markOop prototype_header = k->prototype_header(); + //如果已经有线程对锁对象进行了全局锁定,则取消偏向锁操作 + if (!prototype_header->has_bias_pattern()) { + markOop biased_value = mark; + //CAS 更新对象头markword为非偏向锁 + markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark); + assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked"); + return BIAS_REVOKED; //返回偏向锁撤销状态 + } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { + //如果偏向锁过期,则进入当前分支 + if (attempt_rebias) {//如果允许尝试获取偏向锁 + assert(THREAD->is_Java_thread(), ""); + markOop biased_value = mark; + markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch()); + //通过CAS 操作, 将本线程的 ThreadID 、时间错、分代年龄尝试写入对象头中 + markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark); + if (res_mark == biased_value) { //CAS成功,则返回撤销和重新偏向状态 + return BIAS_REVOKED_AND_REBIASED; + } + } else {//不尝试获取偏向锁,则取消偏向锁 + //通过CAS操作更新分代年龄 + markOop biased_value = mark; + markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); + markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark); + if (res_mark == biased_value) { //如果CAS操作成功,返回偏向锁撤销状态 + return BIAS_REVOKED; + } + } + } + } + ...//省略 +} +``` + +## 偏向锁的撤销 + +当到达一个全局安全点时,这时会根据偏向锁的状态来判断是否需要撤销偏向锁,调用 revoke_at_safepoint方法,这个方法也是在 biasedLocking.cpp中定义的 + + + +```c +void BiasedLocking::revoke_at_safepoint(Handle h_obj) { + assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint"); + oop obj = h_obj(); + //更新撤销偏向锁计数,并返回偏向锁撤销次数和偏向次数 + HeuristicsResult heuristics = update_heuristics(obj, false); + if (heuristics == HR_SINGLE_REVOKE) {//可偏向且未达到批量处理的阈值(下面会单独解释) + revoke_bias(obj, false, false, NULL); //撤销偏向锁 + } else if ((heuristics == HR_BULK_REBIAS) || + (heuristics == HR_BULK_REVOKE)) {//如果是多次撤销或者多次偏向 + //批量撤销 + bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL); + } + clean_up_cached_monitor_info(); +} +``` + +偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,则会升级为轻量级锁,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。 + +> JVM内部为每个类维护了一个偏向锁revoke计数器,对偏向锁撤销进行计数,当这个值达到指定阈值时,JVM会认为这个类的偏向锁有问题,需要重新偏向(rebias),对所有属于这个类的对象进行重偏向的操作成为 批量重偏向(bulk rebias)。在做bulk rebias时,会对这个类的epoch的值做递增,这个epoch会存储在对象头中的epoch字段。在判断这个对象是否获得偏向锁的条件是:markword的 biased_lock:1、lock:01、threadid和当前线程id相等、epoch字段和所属类的epoch值相同,如果epoch的值不一样,要么就是撤销偏向锁、要么就是rebias; 如果这个类的revoke计数器的值继续增加到一个阈值,那么jvm会认为这个类不适合偏向锁,就需要进行bulk revoke操作 + +## 轻量级锁的获取逻辑 + +轻量级锁的获取,是调用 ::slow_enter方法,该方法同样位于 synchronizer.cpp文件中 + + + +```c +void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { + markOop mark = obj->mark(); + assert(!mark->has_bias_pattern(), "should not see bias pattern here"); + + if (mark->is_neutral()) { //如果当前是无锁状态, markword的biase_lock:0,lock:01 + //直接把mark保存到BasicLock对象的_displaced_header字段 + lock->set_displaced_header(mark); + //通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁 + if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { + TEVENT (slow_enter: release stacklock) ; + return ; + } + // Fall through to inflate() ... + } + //如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要争抢锁 + else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { + assert(lock != mark->locker(), "must not re-lock the same lock"); + assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); + lock->set_displaced_header(NULL); + return; + } + +#if 0 + // The following optimization isn't particularly useful. + if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { + lock->set_displaced_header (NULL) ; + return ; + } +#endif + //代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过`inflate`进行膨胀升级为重量级锁 + lock->set_displaced_header(markOopDesc::unused_mark()); + ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); +} +``` + +轻量级锁的获取逻辑简单再整理一下 + +1. mark->is_neutral()方法, is_neutral这个方法是在 markOop.hpp中定义,如果 biased_lock:0且lock:01表示无锁状态 +2. 如果mark处于无锁状态,则进入步骤(3),否则执行步骤(5) +3. 把mark保存到BasicLock对象的displacedheader字段 +4. 通过CAS尝试将markword更新为指向BasicLock对象的指针,如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤(5) +5. 如果当前mark处于加锁状态,且mark中的ptr指针指向当前线程的栈帧,则执行同步代码,否则说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁 + +## 轻量级锁的释放逻辑 + +轻量级锁的释放是通过 monitorexit调用 + + + +```c +IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)) +#ifdef ASSERT + thread->last_frame().interpreter_frame_verify_monitor(elem); +#endif + Handle h_obj(thread, elem->obj()); + assert(Universe::heap()->is_in_reserved_or_null(h_obj()), + "must be NULL or an object"); + if (elem == NULL || h_obj()->is_unlocked()) { + THROW(vmSymbols::java_lang_IllegalMonitorStateException()); + } + ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread); + // Free entry. This must be done here, since a pending exception might be installed on + // exit. If it is not cleared, the exception handling code will try to unlock the monitor again. + elem->set_obj(NULL); +#ifdef ASSERT + thread->last_frame().interpreter_frame_verify_monitor(elem); +#endif +IRT_END +``` + +这段代码中主要是通过 ObjectSynchronizer::slow_exit来执行 + + + +```c +void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) { + fast_exit (object, lock, THREAD) ; +} +``` + +ObjectSynchronizer::fast_exit的代码如下 + + + +```c +void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) { + assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here"); + // if displaced header is null, the previous enter is recursive enter, no-op + markOop dhw = lock->displaced_header(); //获取锁对象中的对象头 + markOop mark ; + if (dhw == NULL) { + // Recursive stack-lock. + // Diagnostics -- Could be: stack-locked, inflating, inflated. + mark = object->mark() ; + assert (!mark->is_neutral(), "invariant") ; + if (mark->has_locker() && mark != markOopDesc::INFLATING()) { + assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ; + } + if (mark->has_monitor()) { + ObjectMonitor * m = mark->monitor() ; + assert(((oop)(m->object()))->mark() == mark, "invariant") ; + assert(m->is_entered(THREAD), "invariant") ; + } + return ; + } + + mark = object->mark() ; //获取线程栈帧中锁记录(LockRecord)中的markword + + // If the object is stack-locked by the current thread, try to + // swing the displaced header from the box back to the mark. + if (mark == (markOop) lock) { + assert (dhw->is_neutral(), "invariant") ; + //通过CAS尝试将Displaced Mark Word替换回对象头,如果成功,表示锁释放成功。 + if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) { + TEVENT (fast_exit: release stacklock) ; + return; + } + } + //锁膨胀,调用重量级锁的释放锁方法 + ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ; +} +``` + +轻量级锁的释放也比较简单,就是将当前线程栈帧中锁记录空间中的Mark Word替换到锁对象的对象头中,如果成功表示锁释放成功。否则,锁膨胀成重量级锁,实现重量级锁的释放锁逻辑 + +## 锁膨胀的过程分析 + +重量级锁是通过对象内部的监视器(monitor)来实现,而monitor的本质是依赖操作系统底层的MutexLock实现的。我们先来看锁的膨胀过程,从前面的分析中已经知道了所膨胀的过程是通过 ObjectSynchronizer::inflate方法实现的,代码如下 + + + +```c +ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { + // Inflate mutates the heap ... + // Relaxing assertion for bug 6320749. + assert (Universe::verify_in_progress() || + !SafepointSynchronize::is_at_safepoint(), "invariant") ; + + for (;;) { //通过无意义的循环实现自旋操作 + const markOop mark = object->mark() ; + assert (!mark->has_bias_pattern(), "invariant") ; + + if (mark->has_monitor()) {//has_monitor是markOop.hpp中的方法,如果为true表示当前锁已经是重量级锁了 + ObjectMonitor * inf = mark->monitor() ;//获得重量级锁的对象监视器直接返回 + assert (inf->header()->is_neutral(), "invariant"); + assert (inf->object() == object, "invariant") ; + assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid"); + return inf ; + } + + if (mark == markOopDesc::INFLATING()) {//膨胀等待,表示存在线程正在膨胀,通过continue进行下一轮的膨胀 + TEVENT (Inflate: spin while INFLATING) ; + ReadStableMark(object) ; + continue ; + } + + if (mark->has_locker()) {//表示当前锁为轻量级锁,以下是轻量级锁的膨胀逻辑 + ObjectMonitor * m = omAlloc (Self) ;//获取一个可用的ObjectMonitor + // Optimistically prepare the objectmonitor - anticipate successful CAS + // We do this before the CAS in order to minimize the length of time + // in which INFLATING appears in the mark. + m->Recycle(); + m->_Responsible = NULL ; + m->OwnerIsThread = 0 ; + m->_recursions = 0 ; + m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class + /**将object->mark_addr()和mark比较,如果这两个值相等,则将object->mark_addr() + 改成markOopDesc::INFLATING(),相等返回是mark,不相等返回的是object->mark_addr()**/ + markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ; + if (cmp != mark) {//CAS失败 + omRelease (Self, m, true) ;//释放监视器 + continue ; // 重试 + } + + markOop dmw = mark->displaced_mark_helper() ; + assert (dmw->is_neutral(), "invariant") ; + + //CAS成功以后,设置ObjectMonitor相关属性 + m->set_header(dmw) ; + + + m->set_owner(mark->locker()); + m->set_object(object); + // TODO-FIXME: assert BasicLock->dhw != 0. + + + guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ; + object->release_set_mark(markOopDesc::encode(m)); + + + if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; + TEVENT(Inflate: overwrite stacklock) ; + if (TraceMonitorInflation) { + if (object->is_instance()) { + ResourceMark rm; + tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", + (void *) object, (intptr_t) object->mark(), + object->klass()->external_name()); + } + } + return m ; //返回ObjectMonitor + } + //如果是无锁状态 + assert (mark->is_neutral(), "invariant"); + ObjectMonitor * m = omAlloc (Self) ; ////获取一个可用的ObjectMonitor + //设置ObjectMonitor相关属性 + m->Recycle(); + m->set_header(mark); + m->set_owner(NULL); + m->set_object(object); + m->OwnerIsThread = 1 ; + m->_recursions = 0 ; + m->_Responsible = NULL ; + m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class + /**将object->mark_addr()和mark比较,如果这两个值相等,则将object->mark_addr() + 改成markOopDesc::encode(m),相等返回是mark,不相等返回的是object->mark_addr()**/ + if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) { + //CAS失败,说明出现了锁竞争,则释放监视器重行竞争锁 + m->set_object (NULL) ; + m->set_owner (NULL) ; + m->OwnerIsThread = 0 ; + m->Recycle() ; + omRelease (Self, m, true) ; + m = NULL ; + continue ; + // interference - the markword changed - just retry. + // The state-transitions are one-way, so there's no chance of + // live-lock -- "Inflated" is an absorbing state. + } + + if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; + TEVENT(Inflate: overwrite neutral) ; + if (TraceMonitorInflation) { + if (object->is_instance()) { + ResourceMark rm; + tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", + (void *) object, (intptr_t) object->mark(), + object->klass()->external_name()); + } + } + return m ; //返回ObjectMonitor对象 + } +} +``` + +锁膨胀的过程稍微有点复杂,整个锁膨胀的过程是通过自旋来完成的,具体的实现逻辑简答总结以下几点 + +1. mark->has_monitor() 判断如果当前锁对象为重量级锁,也就是lock:10,则执行(2),否则执行(3) +2. 通过 mark->monitor获得重量级锁的对象监视器ObjectMonitor并返回,锁膨胀过程结束 +3. 如果当前锁处于 INFLATING,说明有其他线程在执行锁膨胀,那么当前线程通过自旋等待其他线程锁膨胀完成 +4. 如果当前是轻量级锁状态 mark->has_locker(),则进行锁膨胀。首先,通过omAlloc方法获得一个可用的ObjectMonitor,并设置初始数据;然后通过CAS将对象头设置为`markOopDesc:INFLATING,表示当前锁正在膨胀,如果CAS失败,继续自旋 +5. 如果是无锁状态,逻辑类似第4步骤 + +> 锁膨胀的过程实际上是获得一个ObjectMonitor对象监视器,而真正抢占锁的逻辑,在 ObjectMonitor::enter方法里面 + +## 重量级锁的竞争逻辑 + +重量级锁的竞争,在 ObjectMonitor::enter方法中,代码文件在 objectMonitor.cpp重量级锁的代码就不一一分析了,简单说一下下面这段代码主要做的几件事 + +1. 通过CAS将monitor的 _owner字段设置为当前线程,如果设置成功,则直接返回 +2. 如果之前的 _owner指向的是当前的线程,说明是重入,执行 _recursions++增加重入次数 +3. 如果当前线程获取监视器锁成功,将 _recursions设置为1, _owner设置为当前线程 +4. 如果获取锁失败,则等待锁释放 + + + +```c +void ATTR ObjectMonitor::enter(TRAPS) { + // The following code is ordered to check the most common cases first + // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors. + Thread * const Self = THREAD ; + void * cur ; + + cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; + if (cur == NULL) {//CAS成功 + // Either ASSERT _recursions == 0 or explicitly set _recursions = 0. + assert (_recursions == 0 , "invariant") ; + assert (_owner == Self, "invariant") ; + // CONSIDER: set or assert OwnerIsThread == 1 + return ; + } + + if (cur == Self) { + // TODO-FIXME: check for integer overflow! BUGID 6557169. + _recursions ++ ; + return ; + } + + if (Self->is_lock_owned ((address)cur)) { + assert (_recursions == 0, "internal state error"); + _recursions = 1 ; + // Commute owner from a thread-specific on-stack BasicLockObject address to + // a full-fledged "Thread *". + _owner = Self ; + OwnerIsThread = 1 ; + return ; + } + + // We've encountered genuine contention. + assert (Self->_Stalled == 0, "invariant") ; + Self->_Stalled = intptr_t(this) ; + + // Try one round of spinning *before* enqueueing Self + // and before going through the awkward and expensive state + // transitions. The following spin is strictly optional ... + // Note that if we acquire the monitor from an initial spin + // we forgo posting JVMTI events and firing DTRACE probes. + if (Knob_SpinEarly && TrySpin (Self) > 0) { + assert (_owner == Self , "invariant") ; + assert (_recursions == 0 , "invariant") ; + assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; + Self->_Stalled = 0 ; + return ; + } + + assert (_owner != Self , "invariant") ; + assert (_succ != Self , "invariant") ; + assert (Self->is_Java_thread() , "invariant") ; + JavaThread * jt = (JavaThread *) Self ; + assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ; + assert (jt->thread_state() != _thread_blocked , "invariant") ; + assert (this->object() != NULL , "invariant") ; + assert (_count >= 0, "invariant") ; + + // Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy(). + // Ensure the object-monitor relationship remains stable while there's contention. + Atomic::inc_ptr(&_count); + + EventJavaMonitorEnter event; + + { // Change java thread status to indicate blocked on monitor enter. + JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this); + + DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt); + if (JvmtiExport::should_post_monitor_contended_enter()) { + JvmtiExport::post_monitor_contended_enter(jt, this); + } + + OSThreadContendState osts(Self->osthread()); + ThreadBlockInVM tbivm(jt); + + Self->set_current_pending_monitor(this); + + // TODO-FIXME: change the following for(;;) loop to straight-line code. + for (;;) { + jt->set_suspend_equivalent(); + // cleared by handle_special_suspend_equivalent_condition() + // or java_suspend_self() + + EnterI (THREAD) ; + + if (!ExitSuspendEquivalent(jt)) break ; + + // + // We have acquired the contended monitor, but while we were + // waiting another thread suspended us. We don't want to enter + // the monitor while suspended because that would surprise the + // thread that suspended us. + // + _recursions = 0 ; + _succ = NULL ; + exit (false, Self) ; + + jt->java_suspend_self(); + } + Self->set_current_pending_monitor(NULL); + } +...//此处省略无数行代码 +``` + +如果获取锁失败,则需要通过自旋的方式等待锁释放,自旋执行的方法是 ObjectMonitor::EnterI,部分代码如下 + +1. 将当前线程封装成ObjectWaiter对象node,状态设置成TS_CXQ +2. 通过自旋操作将node节点push到_cxq队列 +3. node节点添加到_cxq队列之后,继续通过自旋尝试获取锁,如果在指定的阈值范围内没有获得锁,则通过park将当前线程挂起,等待被唤醒 + + + +```c +void ATTR ObjectMonitor::EnterI (TRAPS) { + Thread * Self = THREAD ; + ...//省略很多代码 + ObjectWaiter node(Self) ; + Self->_ParkEvent->reset() ; + node._prev = (ObjectWaiter *) 0xBAD ; + node.TState = ObjectWaiter::TS_CXQ ; + + // Push "Self" onto the front of the _cxq. + // Once on cxq/EntryList, Self stays on-queue until it acquires the lock. + // Note that spinning tends to reduce the rate at which threads + // enqueue and dequeue on EntryList|cxq. + ObjectWaiter * nxt ; + for (;;) { //自旋,讲node添加到_cxq队列 + node._next = nxt = _cxq ; + if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; + + // Interference - the CAS failed because _cxq changed. Just retry. + // As an optional optimization we retry the lock. + if (TryLock (Self) > 0) { + assert (_succ != Self , "invariant") ; + assert (_owner == Self , "invariant") ; + assert (_Responsible != Self , "invariant") ; + return ; + } + } + ...//省略很多代码 + //node节点添加到_cxq队列之后,继续通过自旋尝试获取锁,如果在指定的阈值范围内没有获得锁,则通过park将当前线程挂起,等待被唤醒 + for (;;) { + if (TryLock (Self) > 0) break ; + assert (_owner != Self, "invariant") ; + + if ((SyncFlags & 2) && _Responsible == NULL) { + Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; + } + + // park self //通过park挂起当前线程 + if (_Responsible == Self || (SyncFlags & 1)) { + TEVENT (Inflated enter - park TIMED) ; + Self->_ParkEvent->park ((jlong) RecheckInterval) ; + // Increase the RecheckInterval, but clamp the value. + RecheckInterval *= 8 ; + if (RecheckInterval > 1000) RecheckInterval = 1000 ; + } else { + TEVENT (Inflated enter - park UNTIMED) ; + Self->_ParkEvent->park() ;//当前线程挂起 + } + + if (TryLock(Self) > 0) break ; //当线程被唤醒时,会从这里继续执行 + + + TEVENT (Inflated enter - Futile wakeup) ; + if (ObjectMonitor::_sync_FutileWakeups != NULL) { + ObjectMonitor::_sync_FutileWakeups->inc() ; + } + ++ nWakeups ; + + if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ; + + if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) { + Self->_ParkEvent->reset() ; + OrderAccess::fence() ; + } + if (_succ == Self) _succ = NULL ; + + // Invariant: after clearing _succ a thread *must* retry _owner before parking. + OrderAccess::fence() ; + } + ...//省略很多代码 +} +``` + +TryLock(self)的代码是在 ObjectMonitor::TryLock定义的,代码的实现如下 + +> 代码的实现原理很简单,通过自旋,CAS设置monitor的_owner字段为当前线程,如果成功,表示获取到了锁,如果失败,则继续被挂起 + + + +```c +int ObjectMonitor::TryLock (Thread * Self) { + for (;;) { + void * own = _owner ; + if (own != NULL) return 0 ; + if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { + // Either guarantee _recursions == 0 or set _recursions = 0. + assert (_recursions == 0, "invariant") ; + assert (_owner == Self, "invariant") ; + // CONSIDER: set or assert that OwnerIsThread == 1 + return 1 ; + } + // The lock had been free momentarily, but we lost the race to the lock. + // Interference -- the CAS failed. + // We can either return -1 or retry. + // Retry doesn't make as much sense because the lock was just acquired. + if (true) return -1 ; + } +} +``` + +## 重量级锁的释放 + +重量级锁的释放是通过 ObjectMonitor::exit来实现的,释放以后会通知被阻塞的线程去竞争锁 + +1. 判断当前锁对象中的owner没有指向当前线程,如果owner指向的BasicLock在当前线程栈上,那么将_owner指向当前线程 +2. 如果当前锁对象中的_owner指向当前线程,则判断当前线程重入锁的次数,如果不为0,继续执行ObjectMonitor::exit(),直到重入锁次数为0为止 +3. 释放当前锁,并根据QMode的模式判断,是否将_cxq中挂起的线程唤醒。还是其他操作 + + + +```c +void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { + Thread * Self = THREAD ; + if (THREAD != _owner) {//如果当前锁对象中的_owner没有指向当前线程 + //如果_owner指向的BasicLock在当前线程栈上,那么将_owner指向当前线程 + if (THREAD->is_lock_owned((address) _owner)) { + // Transmute _owner from a BasicLock pointer to a Thread address. + // We don't need to hold _mutex for this transition. + // Non-null to Non-null is safe as long as all readers can + // tolerate either flavor. + assert (_recursions == 0, "invariant") ; + _owner = THREAD ; + _recursions = 0 ; + OwnerIsThread = 1 ; + } else { + // NOTE: we need to handle unbalanced monitor enter/exit + // in native code by throwing an exception. + // TODO: Throw an IllegalMonitorStateException ? + TEVENT (Exit - Throw IMSX) ; + assert(false, "Non-balanced monitor enter/exit!"); + if (false) { + THROW(vmSymbols::java_lang_IllegalMonitorStateException()); + } + return; + } + } + //如果当前,线程重入锁的次数,不为0,那么就重新走ObjectMonitor::exit,直到重入锁次数为0为止 + if (_recursions != 0) { + _recursions--; // this is simple recursive enter + TEVENT (Inflated exit - recursive) ; + return ; + } + ...//此处省略很多代码 + for (;;) { + if (Knob_ExitPolicy == 0) { + OrderAccess::release_store(&_owner, (void*)NULL); //释放锁 + OrderAccess::storeload(); // See if we need to wake a successor + if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { + TEVENT(Inflated exit - simple egress); + return; + } + TEVENT(Inflated exit - complex egress); + //省略部分代码... + } + //省略部分代码... + ObjectWaiter * w = NULL; + int QMode = Knob_QMode; + //根据QMode的模式判断, + //如果QMode == 2则直接从_cxq挂起的线程中唤醒 + if (QMode == 2 && _cxq != NULL) { + w = _cxq; + ExitEpilog(Self, w); + return; + } + //省略部分代码... 省略的代码为根据QMode的不同,不同的唤醒机制 + } +} +``` + +根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成 + + + +```c +void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { +{ + assert (_owner == Self, "invariant") ; + + // Exit protocol: + // 1. ST _succ = wakee + // 2. membar #loadstore|#storestore; + // 2. ST _owner = NULL + // 3. unpark(wakee) + + _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; + ParkEvent * Trigger = Wakee->_event ; + + // Hygiene -- once we've set _owner = NULL we can't safely dereference Wakee again. + // The thread associated with Wakee may have grabbed the lock and "Wakee" may be + // out-of-scope (non-extant). + Wakee = NULL ; + + // Drop the lock + OrderAccess::release_store_ptr (&_owner, NULL) ; + OrderAccess::fence() ; // ST _owner vs LD in unpark() + + if (SafepointSynchronize::do_call_back()) { + TEVENT (unpark before SAFEPOINT) ; + } + + DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self); + Trigger->unpark() ; //unpark唤醒线程 + + // Maintain stats and report events to JVMTI + if (ObjectMonitor::_sync_Parks != NULL) { + ObjectMonitor::_sync_Parks->inc() ; + } +} +``` \ No newline at end of file diff --git a/week_03/51/volatile_51.md b/week_03/51/volatile_51.md new file mode 100644 index 0000000..85a4f64 --- /dev/null +++ b/week_03/51/volatile_51.md @@ -0,0 +1,69 @@ +#### 一、内存可见性 + +##### 1.1 缓存一致性问题 + +![img](https:////upload-images.jianshu.io/upload_images/1884421-706537dffa7fffeb.png?imageMogr2/auto-orient/strip|imageView2/2/w/531/format/webp) + +>   1、现代计算机系统在存储设备与处理器之间加了一层读写速度尽可能解决处理器运算速度的高速缓存来作为内存与处理器之间的缓冲: 将运算需要使用到的数据复制到缓存中, 让运算能快速进行, 当运算结束后再从缓存同步回内存之中, 这样处理器就无须等待缓慢的内存读写. +>  2、`缓存一致性问题:`在多处理器系统中, 每个处理器都有自己的高速缓存, 而它们又共享同一主内存, 当多个处理器的运算任务都涉及到同一个块主内存区域时, 将可能导致各自的缓存数据不一致. + +##### 1.2 内存模型 + +![img](https:////upload-images.jianshu.io/upload_images/1884421-3a6ec4dc45c1145d.png?imageMogr2/auto-orient/strip|imageView2/2/w/645/format/webp) + +> 在特定的操作协议下, 对特定的内存或高速缓存进行读写访问的过程抽象. + +##### 1.3 内存可见性 + +>   1、一个CPU核心对数据的修改, 对其他CPU核心立即可见. +>  2、CPU修改数据, 首先是对缓存的修改, 然后再同步回主存, 在同步回主存的时候, 如果其他CPU也缓存了这个数据, 就会导致其他CPU缓存上的数据失效, 这样当其他CPU再去它的缓存读取这个数据的时候, 就必须从主存重新获取. +>  3、实现原理一般是基于CPU的`MESI协议`, 其中E表示独占Exclusive, S表示Shared, M表示Modify, I表示Invalid, 如果一个CPU核心修改了数据, 那么这个CPU核心的数据状态就会更新为M, 同时其他核心上的数据状态更新为I, 这个是通过CPU多核之间的嗅探机制实现的. + +##### 1.4 MESI(缓存一致性) + +> `Modify、Exclusive、Shared、Invalid`, 当CPU写数据时, 如果发现操作的变量是共享变量, 即在其他CPU中也存在该变量的副本, 会发出信号通知其他CPU将该变量的缓存行为置为无效状态, 因此当其他CPU需要读取这个变量时, 发现自己缓存中缓存的该变量的缓存行是无效的, 那么它就会从内存中重新读取. + +##### 1.5 嗅探机制 + +> 1、例如在x86处理器下, 将volatile变量修饰的共享变量的Java代码转换成汇编代码, 发现会多了lock修饰. +> 2、Lock前缀的指令在多核处理器下会引发以下事情. +> 3、将当前处理器缓存行的数据写回到系统内存 +> 4、这个写回内存的操作将会使其它CPU里缓存了该内存地址的数据无效. +> 5、`原理分析:`为了提高处理速度, 处理器不直接和内存进行通信, 而是先将系统内存的数据读到内部缓存(L1, L2或其它)后再进行操作, 但操作完就不知道何时会写到内存. 如果对声明了volatile的变量进行写操作, JVM会向处理器发送一条lock前缀的指令. 将这个变量所在的缓存行的数据写回到系统内存. 在多处理器下, 为了保证各个处理器缓存是一致的, 就会实现缓存一致性协议, 每个处理器通过`嗅探`在总线上传播的数据来检查自己的缓存的值是否过期, 当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器缓存行设置成无效状态, 当处理器对这个数据进行修改操作的时候, 会重新匆匆系统内存中把数据读到处理器缓存里. + +##### 1.6 volatile两条实现原则 + +> **`1、Lock前缀指令会引起处理器缓存回写到内存:`** +>  Lock前缀指令导致在执行指令期间, 声言处理器的#LOCK信号. 在多处理器环境中, LOCK#信号确保在声言该信号期间, 处理器可以独占任何共享内存. 但是在最近的处理器里, LOCK#信号一般不锁总线, 而是锁缓存, 毕竟锁总线的开销比较大. 对于Intel486和Pentium处理器, 在锁操作时, 总是在总线上声言LOCK#信号. 但在P6和目前的处理器中, 如果访问的内存区域已经缓存在处理器内部, 则不会声言LOCK#信号. 相反, 它会锁定这块内存区域的缓存并回写到内存, 并使用缓存一致性机制来确保修改的原子性, 此操作被称为"缓存锁定", 缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据. +> **`2、一个处理器的缓存回写到内存会导致其它的缓存无效:`** +>  IA-32处理器和Intel64处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性. 在多核处理器系统中进行操作的时候, IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存, 处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致. 通过嗅探一个处理器来检测其他处理器打算写内存地址, 而这个地址当前处于共享状态, 那么正在嗅探的处理器将使它的缓存行无效, 在下次访问相同内存地址时, 强制执行缓存行填充. + +##### 1.7 Volatile与原子性的关系 + +> Volatile限定的是从缓存读取时刻的校验. + +##### 1.8 原子性 + +##### 1.9 volatile单例模式 + + + +```java +public class Instance { + private static volatile Instance instance; + private Instance() {} + public static Instance getInstance() { + if (instance == null) { + instance = new Instance(); + } + } +} +``` + +> 上面代码分成三步原子指令: +> 1、new指令申请内存; +> 2、在申请的内存中进行Instance的初始化; +> 3、将申请的内存地址的引用赋值给instance变量; +> 虽然volatile可以禁止指令重排序, 让上面三个指令有序执行, 但是问题是volatile并不能保证原子性, 所以上面代码中可能出现的问题是当Thread-A执行到第二步进行new Instance初始化时, 此时还没有将地址值赋给instance变量, 所以Thread-B此时看到的instance==null再次进入if中执行new Instance()操作, 但是为instance赋值堆中的引用是原子操作, 所以此时会进行校验instance是否已经被赋值. +> **`所以假设上面代码被两个线程执行, 那么new Instance()会执行两次, 但是instance赋值操作只会执行一次`** + diff --git a/week_03/51/volatile_51_2.md b/week_03/51/volatile_51_2.md new file mode 100644 index 0000000..2826579 --- /dev/null +++ b/week_03/51/volatile_51_2.md @@ -0,0 +1,334 @@ +## 问题 + +(1)volatile是如何保证可见性的? + +(2)volatile是如何禁止重排序的? + +(3)volatile的实现原理? + +(4)volatile的缺陷? + +## 简介 + +volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决。 + +了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么。 + +## 语义一:可见性 + +前面介绍Java内存模型的时候,我们说过可见性是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。 + +关于Java内存模型的讲解请参考【[死磕 java同步系列之JMM(Java Memory Model)](https://mp.weixin.qq.com/s/jownTN--npu3o8B4c3sbeA)】。 + +而普通变量无法做到立即感知这一点,变量的值在线程之间的传递均需要通过主内存来完成,比如,线程A修改了一个普通变量的值,然后向主内存回写,另外一条线程B只有在线程A的回写完成之后再从主内存中读取变量的值,才能够读取到新变量的值,也就是新变量才能对线程B可见。 + +在这期间可能会出现不一致的情况,比如: + +(1)线程A并不是修改完成后立即回写; + +![volatile](https://gitee.com/alan-tang-tt/yuan/raw/master/%E6%AD%BB%E7%A3%95%20java%E5%90%8C%E6%AD%A5%E7%B3%BB%E5%88%97/resource/volatile1.png) + +(线路A修改了变量x的值为5,但是还没有回写,线程B从主内存读取到的还旧值0) + +(2)线程B还在用着自己工作内存中的值,而并不是立即从主内存读取值; + +![volatile](https://gitee.com/alan-tang-tt/yuan/raw/master/%E6%AD%BB%E7%A3%95%20java%E5%90%8C%E6%AD%A5%E7%B3%BB%E5%88%97/resource/volatile2.png) + +(线程A回写了变量x的值为5到主内存中,但是线程B还没有读取主内存的值,依旧在使用旧值0在进行运算) + +基于以上两种情况,所以,普通变量都无法做到立即感知这一点。 + +但是,volatile变量可以做到立即感知这一点,也就是volatile可以保证可见性。 + +java内存模型规定,volatile变量的每次修改都必须立即回写到主内存中,volatile变量的每次使用都必须从主内存刷新最新的值。 + +![volatile](https://gitee.com/alan-tang-tt/yuan/raw/master/%E6%AD%BB%E7%A3%95%20java%E5%90%8C%E6%AD%A5%E7%B3%BB%E5%88%97/resource/volatile3.png) + +volatile的可见性可以通过下面的示例体现: + +``` +public class VolatileTest { + // public static int finished = 0; + public static volatile int finished = 0; + + private static void checkFinished() { + while (finished == 0) { + // do nothing + } + System.out.println("finished"); + } + + private static void finish() { + finished = 1; + } + + public static void main(String[] args) throws InterruptedException { + // 起一个线程检测是否结束 + new Thread(() -> checkFinished()).start(); + + Thread.sleep(100); + + // 主线程将finished标志置为1 + finish(); + + System.out.println("main finished"); + + } +} +``` + +在上面的代码中,针对finished变量,使用volatile修饰时这个程序可以正常结束,不使用volatile修饰时这个程序永远不会结束。 + +因为不使用volatile修饰时,checkFinished()所在的线程每次都是读取的它自己工作内存中的变量的值,这个值一直为0,所以一直都不会跳出while循环。 + +使用volatile修饰时,checkFinished()所在的线程每次都是从主内存中加载最新的值,当finished被主线程修改为1的时候,它会立即感知到,进而会跳出while循环。 + +## 语义二:禁止重排序 + +前面介绍Java内存模型的时候,我们说过Java中的有序性可以概括为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。 + +前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。 + +关于Java内存模型的讲解请参考【[死磕 java同步系列之JMM(Java Memory Model)](https://mp.weixin.qq.com/s/jownTN--npu3o8B4c3sbeA)】。 + +普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,因为一个线程的方法执行过程中无法感知到这点,这就是“线程内表现为串行的语义”。 + +比如,下面的代码: + +``` +// 两个操作在一个线程 +int i = 0; +int j = 1; +``` + +上面两句话没有依赖关系,JVM在执行的时候为了充分利用CPU的处理能力,可能会先执行`int j = 1;`这句,也就是重排序了,但是在线程内是无法感知的。 + +看似没有什么影响,但是如果是在多线程环境下呢? + +我们再看一个例子: + +``` +public class VolatileTest3 { + private static Config config = null; + private static volatile boolean initialized = false; + + public static void main(String[] args) { + // 线程1负责初始化配置信息 + new Thread(() -> { + config = new Config(); + config.name = "config"; + initialized = true; + }).start(); + + // 线程2检测到配置初始化完成后使用配置信息 + new Thread(() -> { + while (!initialized) { + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + } + + // do sth with config + String name = config.name; + }).start(); + } +} + +class Config { + String name; +} +``` + +这个例子很简单,线程1负责初始化配置,线程2检测到配置初始化完毕,使用配置来干一些事。 + +在这个例子中,如果initialized不使用volatile来修饰,可能就会出现重排序,比如在初始化配置之前把initialized的值设置为了true,这样线程2读取到这个值为true了,就去使用配置了,这时候可能就会出现错误。 + +(此处这个例子只是用于说明重排序,实际运行时很难出现。) + +通过这个例子,彤哥相信大家对“如果在本线程内观察,所有操作都是有序的;在另一个线程观察,所有操作都是无序的”有了更深刻的理解。 + +所以,重排序是站在另一个线程的视角的,因为在本线程中,是无法感知到重排序的影响的。 + +而volatile变量是禁止重排序的,它能保证程序实际运行是按代码顺序执行的。 + +## 实现:内存屏障 + +上面讲了volatile可以保证可见性和禁止重排序,那么它是怎么实现的呢? + +答案就是,内存屏障。 + +内存屏障有两个作用: + +(1)阻止屏障两侧的指令重排序; + +(2)强制把写缓冲区/高速缓存中的数据回写到主内存,让缓存中相应的数据失效; + +关于“内存屏障”的知识点,各路大神的观点也不完全一致,所以这里彤哥也就不展开讲述了,感兴趣的可以看看下面的文章: + +(注意,公众号不允许外发链接,所以只能辛苦复制链接到浏览器中阅读了,而且还可能需要TECH上网) + +(1) Doug Lea的《The JSR-133 Cookbook for Compiler Writers》 + +http://g.oswego.edu/dl/jmm/cookbook.html + +Doug Lea 就是java并发包的作者,大牛! + +(2)Martin Thompson的《Memory Barriers/Fences》 + +https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html + +Martin Thompson 专注于把性能提升到极致,专注于从硬件层面思考问题,比如如何避免伪共享等,大牛! + +它的博客地址就是上面这个地址,里面有很多底层的知识,有兴趣的可以去看看。 + +(3)Dennis Byrne的《Memory Barriers and JVM Concurrency》 + +https://www.infoq.com/articles/memory_barriers_jvm_concurrency + +这是InfoQ英文站上面的一篇文章,我觉得写的挺好的,基本上综合了上面的两种观点,并从汇编层面分析了内存屏障的实现。 + +目前国内市面上的关于内存屏障的讲解基本不会超过这三篇文章,包括相关书籍中的介绍。 + +我们还是来看一个例子来理解内存屏障的影响: + +``` +public class VolatileTest4 { + // a不使用volatile修饰 + public static long a = 0; + // 消除缓存行的影响 + public static long p1, p2, p3, p4, p5, p6, p7; + // b使用volatile修饰 + public static volatile long b = 0; + // 消除缓存行的影响 + public static long q1, q2, q3, q4, q5, q6, q7; + // c不使用volatile修饰 + public static long c = 0; + + public static void main(String[] args) throws InterruptedException { + new Thread(()->{ + while (a == 0) { + long x = b; + } + System.out.println("a=" + a); + }).start(); + + new Thread(()->{ + while (c == 0) { + long x = b; + } + System.out.println("c=" + c); + }).start(); + + Thread.sleep(100); + + a = 1; + b = 1; + c = 1; + } +} +``` + +这段代码中,a和c不使用volatile修饰,b使用volatile修饰,而且我们在a/b、b/c之间各加入7个long字段消除伪共享的影响。 + +关于伪共享的相关知识,可以查看彤哥之前写的文章【[杂谈 什么是伪共享(false sharing)?](https://mp.weixin.qq.com/s/rd13SOSxhLA6TT13N9ni8Q)】。 + +在a和c的两个线程的while循环中我们获取一下b,你猜怎样?如果把`long x = b;`这行去掉呢?运行试试吧。 + +彤哥这里直接说结论了:volatile变量的影响范围不仅仅只包含它自己,它会对其上下的变量值的读写都有影响。 + +## 缺陷 + +上面我们介绍了volatile关键字的两大语义,那么,volatile关键字是不是就是万能的了呢? + +当然不是,忘了我们内存模型那章说的一致性包括的三大特性了么? + +一致性主要包含三大特性:原子性、可见性、有序性。 + +volatile关键字可以保证可见性和有序性,那么volatile能保证原子性么? + +请看下面的例子: + +``` +public class VolatileTest5 { + public static volatile int counter = 0; + + public static void increment() { + counter++; + } + + public static void main(String[] args) throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(100); + IntStream.range(0, 100).forEach(i-> + new Thread(()-> { + IntStream.range(0, 1000).forEach(j->increment()); + countDownLatch.countDown(); + }).start()); + + countDownLatch.await(); + + System.out.println(counter); + } +} +``` + +这段代码中,我们起了100个线程分别对counter自增1000次,一共应该是增加了100000,但是实际运行结果却永远不会达到100000。 + +让我们来看看increment()方法的字节码(IDEA下载相关插件可以查看): + +``` +0 getstatic #2 +3 iconst_1 +4 iadd +5 putstatic #2 +8 return +``` + +可以看到counter++被分解成了四条指令: + +(1)getstatic,获取counter当前的值并入栈 + +(2)iconst_1,入栈int类型的值1 + +(3)iadd,将栈顶的两个值相加 + +(4)putstatic,将相加的结果写回到counter中 + +由于counter是volatile修饰的,所以getstatic会从主内存刷新最新的值,putstatic也会把修改的值立即同步到主内存。 + +但是中间的两步iconst_1和iadd在执行的过程中,可能counter的值已经被修改了,这时并没有重新读取主内存中的最新值,所以volatile在counter++这个场景中并不能保证其原子性。 + +volatile关键字只能保证可见性和有序性,不能保证原子性,要解决原子性的问题,还是只能通过加锁或使用原子类的方式解决。 + +进而,我们得出volatile关键字使用的场景: + +(1)运算的结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值; + +(2)变量不需要与其他状态变量共同参与不变约束。 + +说白了,就是volatile本身不保证原子性,那就要增加其它的约束条件来使其所在的场景本身就是原子的。 + +比如: + +``` +private volatile int a = 0; + +// 线程A +a = 1; + +// 线程B +if (a == 1) { + // do sth +} +``` + +`a = 1;`这个赋值操作本身就是原子的,所以可以使用volatile来修饰。 + +## 总结 + +(1)volatile关键字可以保证可见性; + +(2)volatile关键字可以保证有序性; + +(3)volatile关键字不可以保证原子性; + +(4)volatile关键字的底层主要是通过内存屏障来实现的; + +(5)volatile关键字的使用场景必须是场景本身就是原子的; \ No newline at end of file -- Gitee