面试多线程同步,你必须要思考的问题

新闻
ReentrantLock的实现网上有很多文章了,本篇文章会简单介绍下其java层实现,重点放在分析竞争锁失败后如何阻塞线程。

 ReentrantLock的实现网上有很多文章了,本篇文章会简单介绍下其java层实现,重点放在分析竞争锁失败后如何阻塞线程。 因篇幅有限,synchronized的内容将会放到下篇文章。

[[272324]]

Java Lock的实现

ReentrantLock是jdk中常用的锁实现,其实现逻辑主语基于AQS(juc包中的大多数同步类实现都是基于AQS);接下来会简单介绍AQS的大致原理,关于其实现细节以及各种应用,之后会写一篇文章具体分析。

AQS

AQS是类AbstractQueuedSynchronizer.java的简称,JUC包下的ReentrantLock、CyclicBarrier、CountdownLatch都使用到了AQS。

其大致原理如下:

  1. AQS维护一个叫做state的int型变量和一个双向链表,state用来表示同步状态,双向链表存储的是等待锁的线程
  2. 加锁时首先调用tryAcquire尝试获得锁,如果获得锁失败,则将线程插入到双向链表中,并调用LockSupport.park()方法阻塞当前线程。
  3. 释放锁时调用LockSupport.unpark()唤起链表中的第一个节点的线程。被唤起的线程会重新走一遍竞争锁的流程。

其中tryAcquire方法是抽象方法,具体实现取决于实现类,我们常说的公平锁和非公平锁的区别就在于该方法的实现。

ReentrantLock

ReentrantLock分为公平锁和非公平锁,我们只看公平锁。 ReentrantLock.lock会调用到ReentrantLock#FairSync.lock中:

FairSync.java

  1. static final class FairSync extends Sync { 
  2.  final void lock() { 
  3.  acquire(1); 
  4.  } 
  5.  /** 
  6.  * Fair version of tryAcquire. Don't grant access unless 
  7.  * recursive call or no waiters or is first
  8.  */ 
  9.  protected final boolean tryAcquire(int acquires) { 
  10.  final Thread current = Thread.currentThread(); 
  11.  int c = getState(); 
  12.  if (c == 0) { 
  13.  if (!hasQueuedPredecessors() && 
  14.  compareAndSetState(0, acquires)) { 
  15.  setExclusiveOwnerThread(current); 
  16.  return true
  17.  } 
  18.  } 
  19.  else if (current == getExclusiveOwnerThread()) { 
  20.  int nextc = c + acquires; 
  21.  if (nextc < 0) 
  22.  throw new Error("Maximum lock count exceeded"); 
  23.  setState(nextc); 
  24.  return true
  25.  } 
  26.  return false
  27.  } 
  28.  } 

AbstractQueuedSynchronizer.java

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

可以看到FairSync.lock调用了AQS的acquire方法,而在acquire中首先调用tryAcquire尝试获得锁,以下两种情况返回true:

  1. state==0(代表没有线程持有锁),且等待队列为空(公平的实现),且cas修改state成功。
  2. 当前线程已经获得了锁,这次调用是重入

如果tryAcquire失败则调用acquireQueued阻塞当前线程。acquireQueued最终会调用到LockSupport.park()阻塞线程。

LockSupport.park

个人认为,要深入理解锁机制,一个很重要的点是理解系统是如何阻塞线程的。

LockSupport.java

  1. public static void park(Object blocker) { 
  2.  Thread t = Thread.currentThread(); 
  3.  setBlocker(t, blocker); 
  4.  UNSAFE.park(false, 0L); 
  5.  setBlocker(t, null); 

park方法的参数blocker是用于负责这次阻塞的同步对象,在AQS的调用中,这个对象就是AQS本身。我们知道synchronized关键字是需要指定一个对象的(如果作用于方法上则是当前对象或当前类),与之类似blocker就是LockSupport指定的对象。

park方法调用了native方法UNSAFE.park,第一个参数代表第二个参数是否是绝对时间,第二个参数代表最长阻塞时间。

其实现如下,只保留核心代码,完整代码看查看unsafe.cpp

  1. Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time){ 
  2. ... 
  3. thread->parker()->park(isAbsolute != 0, time); 
  4. ... 

park方法在os_linux.cpp中(其他操作系统的实现在os_xxx中)

  1. void Parker::park(bool isAbsolute, jlong time) { 
  2.  ... 
  3.  //获得当前线程 
  4.  Thread* thread = Thread::current(); 
  5.  assert(thread->is_Java_thread(), "Must be JavaThread"); 
  6.  JavaThread *jt = (JavaThread *)thread; 
  7.  //如果当前线程被设置了interrupted标记,则直接返回 
  8.  if (Thread::is_interrupted(thread, false)) { 
  9.  return
  10.  } 
  11.  if (time > 0) { 
  12.  //unpacktime中根据isAbsolute的值来填充absTime结构体,isAbsolute为true时,time代表绝对时间且单位是毫秒,否则time是相对时间且单位是纳秒 
  13.  //absTime.tvsec代表了对于时间的秒 
  14.  //absTime.tv_nsec代表对应时间的纳秒 
  15.  unpackTime(&absTime, isAbsolute, time); 
  16.  } 
  17.  //调用mutex trylock方法 
  18.  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) { 
  19.  return
  20.  } 
  21.  //_counter是一个许可的数量,跟ReentrantLock里定义的许可变量基本都是一个原理。 unpack方法调用时会将_counter赋值为1。 
  22.  //_counter>0代表已经有人调用了unpark,所以不用阻塞 
  23.  int status ; 
  24.  if (_counter > 0) { // no wait needed 
  25.  _counter = 0; 
  26.  //释放mutex锁 
  27.  status = pthread_mutex_unlock(_mutex); 
  28.  return
  29.  } 
  30. //设置线程状态为CONDVAR_WAIT 
  31.  OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */); 
  32.  ... 
  33.  //等待 
  34.  _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX; 
  35.  pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime); 
  36.  ... 
  37.  //释放mutex锁 
  38.  status = pthread_mutex_unlock(_mutex) ; 

park方法用POSIX的pthread_cond_timedwait方法阻塞线程,调用pthread_cond_timedwait前需要先获得锁,因此park主要流程为:

  1. 调用pthread_mutex_trylock尝试获得锁,如果获取锁失败则直接返回
  2. 调用pthread_cond_timedwait进行等待
  3. 调用pthread_mutex_unlock释放锁

另外,在阻塞当前线程前,会调用OSThreadWaitState的构造方法将线程状态设置为CONDVAR_WAIT,在Jvm中Thread状态枚举如下

  1.  enum ThreadState { 
  2.  ALLOCATED, // Memory has been allocated but not initialized 
  3.  INITIALIZED, // The thread has been initialized but yet started 
  4.  RUNNABLE, // Has been started and is runnable, but not necessarily running 
  5.  MONITOR_WAIT, // Waiting on a contended monitor lock 
  6.  CONDVAR_WAIT, // Waiting on a condition variable 
  7.  OBJECT_WAIT, // Waiting on an Object.wait() call 
  8.  BREAKPOINTED, // Suspended at breakpoint 
  9.  SLEEPING, // Thread.sleep() 
  10.  ZOMBIE // All done, but not reclaimed yet 
  11. }; 

Linux的timedwait

由上文我们可以知道LockSupport.park方法最终是由POSIX的 pthread_cond_timedwait的方法实现的。 我们现在就进一步看看pthread_mutex_trylock,pthread_cond_timedwait,pthread_mutex_unlock这几个方法是如何实现的。

Linux系统中相关代码在glibc库中。

pthread_mutex_trylock

先看trylock的实现, 代码在glibc的pthread_mutex_trylock.c文件中,该方法代码很多,我们只看主要代码

  1. //pthread_mutex_t是posix中的互斥锁结构体 
  2. int 
  3. __pthread_mutex_trylock (mutex) 
  4.  pthread_mutex_t *mutex; 
  5.  int oldval; 
  6.  pid_t id = THREAD_GETMEM (THREAD_SELF, tid); 
  7. switch (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex), 
  8.  PTHREAD_MUTEX_TIMED_NP)) 
  9.  { 
  10.  case PTHREAD_MUTEX_ERRORCHECK_NP: 
  11.  case PTHREAD_MUTEX_TIMED_NP: 
  12.  case PTHREAD_MUTEX_ADAPTIVE_NP: 
  13.  /* Normal mutex. */ 
  14.  if (lll_trylock (mutex->__data.__lock) != 0) 
  15.  break; 
  16.  /* Record the ownership. */ 
  17.  mutex->__data.__owner = id; 
  18.  ++mutex->__data.__nusers; 
  19.  return 0; 
  20.  } 
  21. }  
  22.  //以下代码在lowlevellock.h中  
  23.  #define __lll_trylock(futex) \ 
  24.  (atomic_compare_and_exchange_val_acq (futex, 1, 0) != 0) 
  25.  #define lll_trylock(futex) __lll_trylock (&(futex)) 

mutex默认用的是PTHREAD_MUTEX_NORMAL类型(与PTHREAD_MUTEX_TIMED_NP相同); 因此会先调用lll_trylock方法,lll_trylock实际上是一个cas操作,如果mutex->data.lock==0则将其修改为1并返回0,否则返回1。

如果成功,则更改mutex中的owner为当前线程。

pthread_mutex_unlock

pthread_mutex_unlock.c

  1. int 
  2. internal_function attribute_hidden 
  3. __pthread_mutex_unlock_usercnt (mutex, decr) 
  4.  pthread_mutex_t *mutex; 
  5.  int decr; 
  6.  if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP) 
  7.  == PTHREAD_MUTEX_TIMED_NP) 
  8.  { 
  9.  /* Always reset the owner field. */ 
  10.  normal: 
  11.  mutex->__data.__owner = 0; 
  12.  if (decr) 
  13.  /* One less user. */ 
  14.  --mutex->__data.__nusers; 
  15.  /* Unlock. */ 
  16.  lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)); 
  17.  return 0; 
  18.  } 
  19.  } 

pthread_mutex_unlock将mutex中的owner清空,并调用了lll_unlock方法

lowlevellock.h

  1.  #define __lll_unlock(futex, private) \ 
  2.  ((void) ({ \ 
  3.  int *__futex = (futex); \ 
  4.  int __val = atomic_exchange_rel (__futex, 0); \ 
  5.  \ 
  6.  if (__builtin_expect (__val > 1, 0)) \ 
  7.  lll_futex_wake (__futex, 1, private); \ 
  8.  })) 
  9. #define lll_unlock(futex, private) __lll_unlock(&(futex), private) 
  10. #define lll_futex_wake(ftx, nr, private) \ 
  11. ({ \ 
  12.  DO_INLINE_SYSCALL(futex, 3, (long) (ftx), \ 
  13.  __lll_private_flag (FUTEX_WAKE, private), \ 
  14.  (int) (nr)); \ 
  15.  _r10 == -1 ? -_retval : _retval; \ 
  16. }) 

lll_unlock分为两个步骤:

  1. 将futex设置为0并拿到设置之前的值(用户态操作)
  2. 如果futex之前的值>1,代表存在锁冲突,也就是说有线程调用了FUTEX_WAIT在休眠,所以通过调用系统函数FUTEX_WAKE唤醒休眠线程

FUTEX_WAKE在上一篇文章有分析,futex机制的核心是当获得锁时,尝试cas更改一个int型变量(用户态操作),如果integer原始值是0,则修改成功,该线程获得锁,否则就将当期线程放入到 wait queue中,wait queue中的线程不会被系统调度(内核态操作)。

futex变量的值有3种:0代表当前锁空闲,1代表有线程持有当前锁,2代表存在锁冲突。futex的值初始化时是0;当调用try_lock的时候会利用cas操作改为1(见上面的trylock函数);当调用lll_lock时,如果不存在锁冲突,则将其改为1,否则改为2。

  1. #define __lll_lock(futex, private) \ 
  2.  ((void) ({ \ 
  3.  int *__futex = (futex); \ 
  4.  if (__builtin_expect (atomic_compare_and_exchange_bool_acq (__futex, \ 
  5.  1, 0), 0)) \ 
  6.  { \ 
  7.  if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \ 
  8.  __lll_lock_wait_private (__futex); \ 
  9.  else \ 
  10.  __lll_lock_wait (__futex, private); \ 
  11.  } \ 
  12.  })) 
  13. #define lll_lock(futex, private) __lll_lock (&(futex), private) 
  14. void 
  15. __lll_lock_wait_private (int *futex) 
  16. //第一次进来的时候futex==1,所以不会走这个if 
  17.  if (*futex == 2) 
  18.  lll_futex_wait (futex, 2, LLL_PRIVATE); 
  19. //在这里会把futex设置成2,并调用futex_wait让当前线程等待 
  20.  while (atomic_exchange_acq (futex, 2) != 0) 
  21.  lll_futex_wait (futex, 2, LLL_PRIVATE); 

pthread_cond_timedwait

pthread_cond_timedwait用于阻塞线程,实现线程等待, 代码在glibc的pthread_cond_timedwait.c文件中,代码较长,你可以先简单过一遍,看完下面的分析再重新读一遍代码

  1. int 
  2. int 
  3. __pthread_cond_timedwait (cond, mutex, abstime) 
  4.  pthread_cond_t *cond; 
  5.  pthread_mutex_t *mutex; 
  6.  const struct timespec *abstime; 
  7.  struct _pthread_cleanup_buffer buffer; 
  8.  struct _condvar_cleanup_buffer cbuffer; 
  9.  int result = 0; 
  10.  /* Catch invalid parameters. */ 
  11.  if (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) 
  12.  return EINVAL; 
  13.  int pshared = (cond->__data.__mutex == (void *) ~0l) 
  14.  ? LLL_SHARED : LLL_PRIVATE; 
  15.  //1.获得cond锁 
  16.  lll_lock (cond->__data.__lock, pshared); 
  17.  //2.释放mutex锁 
  18.  int err = __pthread_mutex_unlock_usercnt (mutex, 0); 
  19.  if (err) 
  20.  { 
  21.  lll_unlock (cond->__data.__lock, pshared); 
  22.  return err; 
  23.  } 
  24.  /* We have one new user of the condvar. */ 
  25.  //每执行一次wait(pthread_cond_timedwait/pthread_cond_wait),__total_seq就会+1 
  26.  ++cond->__data.__total_seq; 
  27.  //用来执行futex_wait的变量 
  28.  ++cond->__data.__futex; 
  29.  //标识该cond还有多少线程在使用,pthread_cond_destroy需要等待所有的操作完成 
  30.  cond->__data.__nwaiters += 1 << COND_NWAITERS_SHIFT; 
  31.  /* Remember the mutex we are using here. If there is already a 
  32.  different address store this is a bad user bug. Do not store 
  33.  anything for pshared condvars. */ 
  34.  //保存mutex锁 
  35.  if (cond->__data.__mutex != (void *) ~0l) 
  36.  cond->__data.__mutex = mutex; 
  37.  /* Prepare structure passed to cancellation handler. */ 
  38.  cbuffer.cond = cond; 
  39.  cbuffer.mutex = mutex; 
  40.  /* Before we block we enable cancellation. Therefore we have to 
  41.  install a cancellation handler. */ 
  42.  __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer); 
  43.  /* The current values of the wakeup counter. The "woken" counter 
  44.  must exceed this value. */ 
  45.  //记录futex_wait前的__wakeup_seq(为该cond上执行了多少次sign操作+timeout次数)和__broadcast_seq(代表在该cond上执行了多少次broadcast) 
  46.  unsigned long long int val; 
  47.  unsigned long long int seq; 
  48.  val = seq = cond->__data.__wakeup_seq; 
  49.  /* Remember the broadcast counter. */ 
  50.  cbuffer.bc_seq = cond->__data.__broadcast_seq; 
  51.  while (1) 
  52.  { 
  53.  //3.计算要wait的相对时间 
  54.  struct timespec rt; 
  55.  { 
  56. #ifdef __NR_clock_gettime 
  57.  INTERNAL_SYSCALL_DECL (err); 
  58.  int ret; 
  59.  ret = INTERNAL_VSYSCALL (clock_gettime, err, 2, 
  60.  (cond->__data.__nwaiters 
  61.  & ((1 << COND_NWAITERS_SHIFT) - 1)), 
  62.  &rt); 
  63. # ifndef __ASSUME_POSIX_TIMERS 
  64.  if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (ret, err), 0)) 
  65.  { 
  66.  struct timeval tv; 
  67.  (void) gettimeofday (&tv, NULL); 
  68.  /* Convert the absolute timeout value to a relative timeout. */ 
  69.  rt.tv_sec = abstime->tv_sec - tv.tv_sec; 
  70.  rt.tv_nsec = abstime->tv_nsec - tv.tv_usec * 1000; 
  71.  } 
  72.  else 
  73. # endif 
  74.  { 
  75.  /* Convert the absolute timeout value to a relative timeout. */ 
  76.  rt.tv_sec = abstime->tv_sec - rt.tv_sec; 
  77.  rt.tv_nsec = abstime->tv_nsec - rt.tv_nsec; 
  78.  } 
  79. #else 
  80.  /* Get the current time. So far we support only one clock. */ 
  81.  struct timeval tv; 
  82.  (void) gettimeofday (&tv, NULL); 
  83.  /* Convert the absolute timeout value to a relative timeout. */ 
  84.  rt.tv_sec = abstime->tv_sec - tv.tv_sec; 
  85.  rt.tv_nsec = abstime->tv_nsec - tv.tv_usec * 1000; 
  86. #endif 
  87.  } 
  88.  if (rt.tv_nsec < 0) 
  89.  { 
  90.  rt.tv_nsec += 1000000000; 
  91.  --rt.tv_sec; 
  92.  } 
  93.  /*---计算要wait的相对时间 end---- */ 
  94.  //是否超时 
  95.  /* Did we already time out? */ 
  96.  if (__builtin_expect (rt.tv_sec < 0, 0)) 
  97.  { 
  98.  //被broadcast唤醒,这里疑问的是,为什么不需要判断__wakeup_seq? 
  99.  if (cbuffer.bc_seq != cond->__data.__broadcast_seq) 
  100.  goto bc_out; 
  101.  goto timeout; 
  102.  } 
  103.  unsigned int futex_val = cond->__data.__futex; 
  104.  //4.释放cond锁,准备wait 
  105.  lll_unlock (cond->__data.__lock, pshared); 
  106.  /* Enable asynchronous cancellation. Required by the standard. */ 
  107.  cbuffer.oldtype = __pthread_enable_asynccancel (); 
  108.  //5.调用futex_wait 
  109.  /* Wait until woken by signal or broadcast. */ 
  110.  err = lll_futex_timed_wait (&cond->__data.__futex, 
  111.  futex_val, &rt, pshared); 
  112.  /* Disable asynchronous cancellation. */ 
  113.  __pthread_disable_asynccancel (cbuffer.oldtype); 
  114.  //6.重新获得cond锁,因为又要访问&修改cond的数据了 
  115.  lll_lock (cond->__data.__lock, pshared); 
  116.  //__broadcast_seq值发生改变,代表发生了有线程调用了广播 
  117.  if (cbuffer.bc_seq != cond->__data.__broadcast_seq) 
  118.  goto bc_out; 
  119.  //判断是否是被sign唤醒的,sign会增加__wakeup_seq 
  120.  //第二个条件cond->__data.__woken_seq != val的意义在于 
  121.  //可能两个线程A、B在wait,一个线程调用了sign导致A被唤醒,这时B因为超时被唤醒 
  122.  //对于B线程来说,执行到这里时第一个条件也是满足的,从而导致上层拿到的result不是超时 
  123.  //所以这里需要判断下__woken_seq(即该cond已经被唤醒的线程数)是否等于__wakeup_seq(sign执行次数+timeout次数) 
  124.  val = cond->__data.__wakeup_seq; 
  125.  if (val != seq && cond->__data.__woken_seq != val) 
  126.  break; 
  127.  /* Not woken yet. Maybe the time expired? */ 
  128.  if (__builtin_expect (err == -ETIMEDOUT, 0)) 
  129.  { 
  130.  timeout: 
  131.  /* Yep. Adjust the counters. */ 
  132.  ++cond->__data.__wakeup_seq; 
  133.  ++cond->__data.__futex; 
  134.  /* The error value. */ 
  135.  result = ETIMEDOUT; 
  136.  break; 
  137.  } 
  138.  } 
  139.  //一个线程已经醒了所以这里__woken_seq +1 
  140.  ++cond->__data.__woken_seq; 
  141.  bc_out: 
  142.  // 
  143.  cond->__data.__nwaiters -= 1 << COND_NWAITERS_SHIFT; 
  144.  /* If pthread_cond_destroy was called on this variable already, 
  145.  notify the pthread_cond_destroy caller all waiters have left 
  146.  and it can be successfully destroyed. */ 
  147.  if (cond->__data.__total_seq == -1ULL 
  148.  && cond->__data.__nwaiters < (1 << COND_NWAITERS_SHIFT)) 
  149.  lll_futex_wake (&cond->__data.__nwaiters, 1, pshared); 
  150.  //9.cond数据修改完毕,释放锁 
  151.  lll_unlock (cond->__data.__lock, pshared); 
  152.  /* The cancellation handling is back to normal, remove the handler. */ 
  153.  __pthread_cleanup_pop (&buffer, 0); 
  154.  //10.重新获得mutex锁 
  155.  err = __pthread_mutex_cond_lock (mutex); 
  156.  return err ?: result; 

上面的代码虽然加了注释,但相信大多数人第一次看都看不懂。 我们来简单梳理下,上面代码有两把锁,一把是mutex锁,一把cond锁。另外,在调用pthread_cond_timedwait前后必须调用pthread_mutex_lock(&mutex);和pthread_mutex_unlock(&mutex);加/解mutex锁。

因此pthread_cond_timedwait的使用大致分为几个流程:

  1. 加mutex锁(在pthread_cond_timedwait调用前)
  2. 加cond锁
  3. 释放mutex锁
  4. 修改cond数据
  5. 释放cond锁
  6. 执行futex_wait
  7. 重新获得cond锁
  8. 比较cond的数据,判断当前线程是被正常唤醒的还是timeout唤醒的,需不需要重新wait
  9. 修改cond数据
  10. 是否cond锁
  11. 重新获得mutex锁
  12. 释放mutex锁(在pthread_cond_timedwait调用后)

看到这里,你可能有几点疑问:为什么需要两把锁?mutex锁和cond锁的作用是什么?

mutex锁

说mutex锁的作用之前,我们回顾一下java的Object.wait的使用。Object.wait必须是在synchronized同步块中使用。试想下如果不加synchronized也能运行Object.wait的话会存在什么问题?

  1. Object condObj=new Object(); 
  2. voilate int flag = 0; 
  3. public void waitTest(){ 
  4.  if(flag == 0){ 
  5.  condObj.wait(); 
  6.  } 
  7. public void notifyTest(){ 
  8.  flag=1; 
  9.  condObj.notify(); 

如上代码,A线程调用waitTest,这时flag==0,所以准备调用wait方法进行休眠,这时B线程开始执行,调用notifyTest将flag置为1,并调用notify方法,注意:此时A线程还没调用wait,所以notfiy没有唤醒任何线程。然后A线程继续执行,调用wait方法进行休眠,而之后不会有人来唤醒A线程,A线程将永久wait下去!

  1. Object condObj=new Object(); 
  2. voilate int flag = 0; 
  3. public void waitTest(){ 
  4.  synchronized(condObj){ 
  5.  if(flag == 0){ 
  6.  condObj.wait(); 
  7.  } 
  8.  } 
  9. public void notifyTest(){ 
  10.  synchronized(condObj){ 
  11.  flag=1; 
  12.  condObj.notify(); 
  13.  } 

在有锁保护下的情况下, 当调用condObj.wait时,flag一定是等于0的,不会存在一直wait的问题。

回到pthread_cond_timedwait,其需要加mutex锁的原因就呼之欲出了: 保证wait和其wait条件的原子性

不管是glibc的pthread_cond_timedwait/pthread_cond_signal还是java层的Object.wait/Object.notify,Jdk AQS的Condition.await/Condition.signal,所有的Condition机制都需要在加锁环境下才能使用,其根本原因就是要保证进行线程休眠时,条件变量是没有被篡改的。

注意下mutex锁释放的时机,回顾上文中pthread_cond_timedwait的流程,在第2步时就释放了mutex锁,之后调用futex_wait进行休眠,为什么要在休眠前就释放mutex锁呢?原因也很简单:如果不释放mutex锁就开始休眠,那其他线程就永远无法调用signal方法将休眠线程唤醒(因为调用signal方法前需要获得mutex锁)。

在线程被唤醒之后还要在第10步中重新获得mutex锁是为了保证锁的语义(思考下如果不重新获得mutex锁会发生什么)。

cond锁

cond锁的作用其实很简单: 保证对象cond->data的线程安全。 在pthread_cond_timedwait时需要修改cond->data的数据,如增加total_seq(在这个cond上一共执行过多少次wait)增加nwaiters(现在还有多少个线程在wait这个cond),所有在修改及访问cond->data时需要加cond锁。

这里我没想明白的一点是,用mutex锁也能保证cond->data修改的线程安全,只要晚一点释放mutex锁就行了。为什么要先释放mutex,重新获得cond来保证线程安全? 是为了避免mutex锁住的范围太大吗?

该问题的答案可以见评论区@11800222 的回答:

mutex锁不能保护cond->data修改的线程安全,调用signal的线程没有用mutex锁保护修改cond的那段临界区。

pthread_cond_wait/signal这一对本身用cond锁同步就能睡眠唤醒。 wait的时候需要传入mutex是因为睡眠前需要释放mutex锁,但睡眠之前又不能有无锁的空隙,解决办法是让mutex锁在cond锁上之后再释放。 而signal前不需要释放mutex锁,在持有mutex的情况下signal,之后再释放mutex锁。

如何唤醒休眠线程

唤醒休眠线程的代码比较简单,主要就是调用lll_futex_wake。

  1. int 
  2. __pthread_cond_signal (cond) 
  3.  pthread_cond_t *cond; 
  4.  int pshared = (cond->__data.__mutex == (void *) ~0l) 
  5.  ? LLL_SHARED : LLL_PRIVATE; 
  6.  //因为要操作cond的数据,所以要加锁 
  7.  lll_lock (cond->__data.__lock, pshared); 
  8.  /* Are there any waiters to be woken? */ 
  9.  if (cond->__data.__total_seq > cond->__data.__wakeup_seq) 
  10.  { 
  11.  //__wakeup_seq为执行sign与timeout次数的和 
  12.  ++cond->__data.__wakeup_seq; 
  13.  ++cond->__data.__futex; 
  14.  ... 
  15.  //唤醒wait的线程 
  16.  lll_futex_wake (&cond->__data.__futex, 1, pshared); 
  17.  } 
  18.  /* We are done. */ 
  19.  lll_unlock (cond->__data.__lock, pshared); 
  20.  return 0; 

End

本文对Java简单介绍了ReentrantLock实现原理,对LockSupport.park底层实现pthread_cond_timedwait机制做了详细分析。

责任编辑:华轩 来源: java520
相关推荐

2019-02-18 13:36:03

Redis数据库面试

2011-06-22 13:47:16

Java多线程

2011-06-22 13:57:54

Java多线程

2022-09-05 17:49:53

Java线程池

2009-07-01 17:34:03

Servlet和JSP

2019-08-06 14:54:22

Hadoop数据集海量数据

2019-03-23 20:00:04

面试react.js前端

2013-05-29 10:47:50

Android开发Java多线程java面试题

2018-05-30 16:55:47

阿里Java多线程

2018-09-21 11:11:34

备份离线自动

2010-01-21 11:27:30

linux多线程机制线程同步

2018-11-08 12:07:38

备份手动磁盘

2018-11-28 10:00:42

React组件前端

2019-06-20 17:39:12

Android启动优化

2012-06-05 02:12:55

Java多线程

2015-07-22 09:39:38

IOS多线程同步

2015-07-22 09:51:51

iOS开发线程

2019-05-16 15:35:36

2023-04-26 16:34:12

2021-10-21 08:13:11

Springboot
点赞
收藏

51CTO技术栈公众号