Java并发——基石篇(下)

Object wait和notify的实现机制

Java Object类提供了一个基于native实现的wait和notify线程间通讯的方式,这是除了synchronized之外的另外一块独立的并发基础部分,有关wait和notify·的部分内容,我们在上面分析monitor的exit的时候已经有一些涉及,但是并没有过多的深入,留下了不少的疑问,本小节会详细分析。

wait实现

ObjectMonitor类中的wait函数代码实现如下:

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
  ...
  if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
    ...
    // 抛出异常,不会直接进入等待     THROW(vmSymbols::java_lang_InterruptedException());
    ...
  }
  ...
  ObjectWaiter node(Self);
  node.TState = ObjectWaiter::TS_WAIT;
  Self->_ParkEvent->reset();
  OrderAccess::fence();

  Thread::SpinAcquire(&_WaitSetLock, "WaitSet - add");
  AddWaiter(&node);
  Thread::SpinRelease(&_WaitSetLock);

  if ((SyncFlags & 4) == 0) {
    _Responsible = NULL;
  }

  ...
  // exit the monitor   exit(true, Self); 
  ...
  if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
        // Intentionally empty       } else if (node._notified == 0) {
        if (millis <= 0) {
          Self->_ParkEvent->park();
        } else {
          ret = Self->_ParkEvent->park(millis);
        }
  }
  // 被 notify 唤醒之后的善后逻辑   ...
}

照例只列出wait函数的核心功能部分,首先会判断一下当前线程是否为可中断并且是否已经被中断,如果是的话会直接抛出InterruptedException异常,而不会进入wait等待,否则的话,就需要执行下面的等待过程,首先会根据Self当前线程新建一个ObjectWaiter对象节点,这个对象我们在前面分析monitor的enter的视乎就已经见过了。生成一个新的节点之后就是需要将这个节点放到等待队列中,通过调用AddWaiter函数实现node的入队操作,不过在入队操作之前需要获得互斥锁以保证并发安全:

void Thread::SpinAcquire(volatile int * adr, const char * LockName) {
  if (Atomic::cmpxchg (1, adr, 0) == 0) {
    return;   // normal fast-path return   }

  // Slow-path : We've encountered contention -- Spin/Yield/Block strategy.   TEVENT(SpinAcquire - ctx);
  int ctr = 0;
  int Yields = 0;
  for (;;) {
    while (*adr != 0) {
      ++ctr;
      if ((ctr & 0xFFF) == 0 || !os::is_MP()) {
        if (Yields > 5) {
          os::naked_short_sleep(1);
        } else {
          os::naked_yield();
          ++Yields;
        }
      } else {
        SpinPause();
      }
    }
    if (Atomic::cmpxchg(1, adr, 0) == 0) return;
  }
}

SpinAcquire是一个自旋锁实现,它通过一个死循环不断通过cas检查判断是否获得锁,这里开始会通过一个cas检查看下是否能够成功,如果成功的话就不用进行下面比较重量级的spin过程,如果获取失败,就需要进入下面的spin过程,这里的spin逻辑是一个比较有意思的算法。这里定义了一个ctr变量,其实就是counter计数器的意思,(ctr&0xFFF)==0|| !os::is_MP()这个条件比较有意思,意思是如果我尝试的次数大于)0xfff,或者当前系统是一个单核处理器系统,那么就执行下面的逻辑。可以看到这里的spin是有一定的限度的,首先开始的时候,如果是多核系统,那么会直接执行SpinPause,我们看下SpinPause函数的实现,这个函数是实现CPU的忙等待,因此会有不同系统和CPU架构的对应实现。SpinPause函数linux平台代码如下:

int SpinPause() {
    return 0;
}

即SpinPause函数直接返回0,是SpinAcquire实现CPU忙等待的一种方式,此外,如果SpinAcquire里尝试的次数已经到了0xFFF次的话,就利用另一种方式实现等待:

if (Yields > 5) {
   os::naked_short_sleep(1);
} else {
   os::naked_yield();
   ++Yields;
}

首先会尝试通过yield函数来将当前线程的CPU执行时间让出来,如果让了5次还是没有获得锁,那么就只能通过naked_short_sleep来实现等待了,这里的naked_short_sleep函数从名字就可以看出来是短暂休眠等待,通过每次休眠等待1ms实现。我们现在看下naked_yield的实现方式,同样看linux平台的实现:

void os::naked_yield() {
  sched_yield();
}

可以看到这里的实现是直接调用pthread的sched_yield函数实现线程的时间片让出。接下来看linux平台naked_short_sleep的实现:

void os::naked_short_sleep(jlong ms) {
  struct timespec req;

  assert(ms < 1000, "Un-interruptable sleep, short time use only");
  req.tv_sec = 0;
  if (ms > 0) {
    req.tv_nsec = (ms % 1000) * 1000000;
  } else {
    req.tv_nsec = 1;
  }

  nanosleep(&req, NULL);

  return;
}

这里我们通过nanosleep系统调用实现线程的timed waiting。

到这里我们分析一下SpinAcquire的实现逻辑:如果是单核处理器就通过yield或者sleep实现等待,如果是多核处理器的话就通过调用空实现函数来忙等待。因为如果是单核CPU的话,你通过调用空实现函数实现忙等待是不科学的,因为只有一个核,如果通过这个核来实现忙等待,那么原本需要释放锁的线程得不到执行,那就可能造成饥饿等待,我们的CPU一直在转动,但是没有解决任何问题。所以如果是单核CPU系统的话,我们不能通过调用空函数来实现等待。相反,如果是多核的话,那就可以在另一个空闲的CPU上实现忙等待增加系统的吞吐量,可以看到在JVM中为了增加系统的算力和保证系统的兼容性,做了多少努力和实现。

上面的SpinAcquire函数返回之后,就表示我们获得了锁,现在可以将我们的node放到等待队列中了:

inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  assert(node != NULL, "should not add NULL node");
  assert(node->_prev == NULL, "node already in list");
  assert(node->_next == NULL, "node already in list");
  // put node at end of queue (circular doubly linked list)   if (_WaitSet == NULL) {
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    ObjectWaiter* head = _WaitSet;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}

这里的实现比较简单,就是讲node插入双向链表_WaitSet的尾部。插入链表完毕知乎,需要通过SpinRelease将锁释放。

将新建的node节点加入到WaitSet队列中了,我们接着看wait函数接下来的逻辑,现在我们就要执行如下内容:

// exit the monitor exit(true, Self);

wait操作释放monitor锁就是在这里实现的。然后接着的是wait函数的park等待。

if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
        // Intentionally empty } else if (node._notified == 0) {
    if (millis <= 0) {
        Self->_ParkEvent->park();
    } else {
        ret = Self->_ParkEvent->park(millis);
    }
}

在正式park之前,还会再一次看下是否有interruptd,如果有的话就会跳过park操作,否则就会进行park阻塞,park阻塞的时间就是wait函数调用时传入的时间参数。
wait函数接下来的操作是park阻塞唤醒之后的善后逻辑,对于我们的分析不是很重要,这里就跳过。

notify实现

notify函数的实现代码如下:

void ObjectMonitor::notify(TRAPS) {
  CHECK_OWNER();
  if (_WaitSet == NULL) {
    TEVENT(Empty-Notify);
    return;
  }
  DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
  INotify(THREAD);
  OM_PERFDATA_OP(Notifications, inc(1));
}

这里主要通过判断WaitSet队列中是否还有线程执行了wait,如果没有就直接返回,如果有就对线程进行唤醒,唤醒通过调用INotify函数实现:

void ObjectMonitor::INotify(Thread * Self) {
  const int policy = Knob_MoveNotifyee;

  Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
  ObjectWaiter * iterator = DequeueWaiter();
  if (iterator != NULL) {
    ObjectWaiter * list = _EntryList;
    if (policy == 0) {
      // prepend to EntryList       if (list == NULL) {
        ...
      } else {
        ...
      }
    } else if (policy == 1) {
      // append to EntryList       if (list == NULL) {
        ...
      } else {
        ...
      }
    } else if (policy == 2) {
      // prepend to cxq       if (list == NULL) {
        ...
      } else {
        ...
      }
    } else if (policy == 3) {
      // append to cxq       ...
    } else {
      ...
    }
    ...
  }
  Thread::SpinRelease(&_WaitSetLock);
}

可以看到,这里的操作都是在_WaitSetLock保护下的,首先会从WaitSet队列中出队一个节点,然后针对这个节点根据Knob_MoveNotifyee来决定执行不同的策略逻辑,并且策略中的逻辑框架就是一样的,根据_EntryList是否为空执行不同操作。Knob_MoveNottifyee默认值为2。
notify的唤醒策略主要有以下几种:

  1. 策略 0:将需要唤醒的 node 放到 EntryList 的头部
  2. 策略 1:将需要唤醒的 node 放到 EntryList 的尾部
  3. 策略 2:将需要唤醒的 node 放到 CXQ 的头部
  4. 策略 3:将需要唤醒的 node 放到 CXQ 的尾部

在分析不同策略的逻辑之前,我们先看下WaitSet的出队逻辑实现,这是INotify函数开始会执行的事:

inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
  // dequeue the very first waiter   ObjectWaiter* waiter = _WaitSet;
  if (waiter) {
    DequeueSpecificWaiter(waiter);
  }
  return waiter;
}

从注释中可以看出,这里将WaitSet队列中的第一个node出队,下面直接返回WaitSet队列指针也就是队头,然后删除出队节点:

inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) {
  assert(node != NULL, "should not dequeue NULL node");
  assert(node->_prev != NULL, "node already removed from list");
  assert(node->_next != NULL, "node already removed from list");
  // when the waiter has woken up because of interrupt,   // timeout or other spurious wake-up, dequeue the   // waiter from waiting list   ObjectWaiter* next = node->_next;
  if (next == node) {
    assert(node->_prev == node, "invariant check");
    _WaitSet = NULL;
  } else {
    ObjectWaiter* prev = node->_prev;
    assert(prev->_next == node, "invariant check");
    assert(next->_prev == node, "invariant check");
    next->_prev = prev;
    prev->_next = next;
    if (_WaitSet == node) {
      _WaitSet = next;
    }
  }
  node->_next = NULL;
  node->_prev = NULL;
}

这样我们就完成了从WaitSet双向链表队列中的队头出队逻辑。

唤醒策略0

if (list == NULL) {
    iterator->_next = iterator->_prev = NULL;
    _EntryList = iterator;
} else {
    list->_prev = iterator;
    iterator->_next = list;
    iterator->_prev = NULL;
    _EntryList = iterator;
}

如果EntryList为空的话,表示之前没有线程被notify唤醒,已经直接将当前节点放到EntryList中即可,否则的话,就将当前节点放到EntryList的头部。

唤醒策略1

策略1和策略0逻辑很相似,这里只是将节点放到尾部:

if (list == NULL) {
        iterator->_next = iterator->_prev = NULL;
        _EntryList = iterator;
} else {
        // CONSIDER:  finding the tail currently requires a linear-time walk of         // the EntryList.  We can make tail access constant-time by converting to         // a CDLL instead of using our current DLL.         ObjectWaiter * tail;
        for (tail = list; tail->_next != NULL; tail = tail->_next) {}
        assert(tail != NULL && tail->_next == NULL, "invariant");
        tail->_next = iterator;
        iterator->_prev = tail;
        iterator->_next = NULL;
}

唤醒策略2

if (list == NULL) {
        iterator->_next = iterator->_prev = NULL;
        _EntryList = iterator;
} else {
        iterator->TState = ObjectWaiter::TS_CXQ;
        for (;;) {
          ObjectWaiter * front = _cxq;
          iterator->_next = front;
          if (Atomic::cmpxchg(iterator, &_cxq, front) == front) {
            break;
          }
        }
}

首先如果发现 EntryList 为空的话,也就是第一个被 notify 唤醒的线程会进入到 EntryList,而 WaitSet 中剩下的节点会依次插入到 cxq 的头部,然后更新 cxq 指针指向新的头节点。

唤醒策略 3

策略3的逻辑和策略2比较相似,只是策略3会将节点放到cxq尾部:

iterator->TState = ObjectWaiter::TS_CXQ;
      for (;;) {
        ObjectWaiter * tail = _cxq;
        if (tail == NULL) {
          iterator->_next = NULL;
          if (Atomic::replace_if_null(iterator, &_cxq)) {
            break;
          }
        } else {
          while (tail->_next != NULL) tail = tail->_next;
          tail->_next = iterator;
          iterator->_prev = tail;
          iterator->_next = NULL;
          break;
        }
}

这里不会判断 EntryList 是否为空,而是直接将节点放到 cxq 的尾部,这一点和前面几个策略不一样,需要注意下。

notifyAll 实现

notifyAll 的实现其实和 notify 实现大同小异:

void ObjectMonitor::notifyAll(TRAPS) {
  CHECK_OWNER();
  if (_WaitSet == NULL) {
    TEVENT(Empty-NotifyAll);
    return;
  }

  DTRACE_MONITOR_PROBE(notifyAll, this, object(), THREAD);
  int tally = 0;
  while (_WaitSet != NULL) {
    tally++;
    INotify(THREAD);
  }

  OM_PERFDATA_OP(Notifications, inc(tally));
} 

可以看到,其实就是根据WaitSet长度,反复调用INotify函数,相当于多次调用 notify。

你可能感兴趣的:(java)