条件锁

ReentrantLock类有一个方法newCondition用来生成这个锁对象的一个条件(ConditionObject)对象,它实现了Condition接口。Condition提供了线程通讯的一套机制await和signal等线程间进行通讯的方法。。

1、适用场景

当某线程获取了锁对象,但因为某些条件没有满足,需要在这个条件上等待,直到条件满足才能够往下继续执行时,就需要用到条件锁。

这种情况下,线程主动在某条件上阻塞,当其它线程发现条件发生变化时,就可以唤醒阻塞在此条件上的线程。

2、使用示例

下面是来自JDK的一段示例代码,需要先获得某个锁对象之后,才能调用这个锁的条件对象进行阻塞。

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

注意上面的代码,先是通过lock.lock获得了锁对象,然后发现条件不满足时(count==items.length),缓存已满,无法继续往里面写入数据,这时候就调用条件对象notFull.await()进行阻塞。

如果条件满足,就会往缓存中写入数据,同时通知等待缓存非空的线程,notEmpty.signal.

这样就实现了读线程和写线程之间的通讯

3、线程阻塞对锁的影响

上面的例子中,线程是先获得了锁对象之后,然后调用notFull.await进行的线程阻塞。在这种情况下,拥有锁的线程进入阻塞,是否可能会造成死锁。

答案当然是否定的。因为线程在调用条件对象的await方法中,首先会释放当前的锁,然后才让自己进入阻塞状态,等待唤醒。

4、线程的条件等待、唤醒与锁对象的关系

在ReentrantLock解析中说过,AbstractQueuedSynchronizer的内部维护了一个队列,等待该锁的线程是在这个队列中。类似的,ConditionObject内部也是维护了一个队列,等待该条件的线程也构成了一个队列。

当现成调用await进入阻塞时,便会加入到ConditionObject内部的等待队列中。注意,这里是自己主动进入阻塞,除非被其它线程唤醒或者被中断,否则线程将一直阻塞下去。

当其它线程调用signal唤醒阻塞的线程时,便把等待队列中的第一个节点从队列中移除,同时把节点加入到AbstractQueuedSynchronizer
锁对象内的等待队列中。为什么是进入到锁的等待队列中?因为线程被唤醒之后,并不意味着就能立刻执行。此时,其它线程有可能正好拥有这个锁,前面也已经有现成在等待这个锁,所以被唤醒的线程需要进入锁的等待队列中,在前面的线程执行完成后,才能继续后续的操作。

可参考下图

5、线程是否能同时处于条件对象的等待队列中和锁对象的等待队列中

不能。线程只有调用条件对象的await方法,才能进入这个条件对象的等待队列中。而线程在调用await方法的前提是线程已经获取了锁,所以线程是在拥有锁的状态下进入条件对象的等待队列的,拥有锁的线程也就是正在运行的线程,是不在锁对象的等待队列中的。

只有当一个线程试着获取锁的时候,而这个锁正好又由其它线程占据的时候,线程才会进入锁的等待队列中,等待拥有锁的线程执行完成,释放锁的时候被唤醒。

6、实现原理

相关代码在AbstractQueuedSynchronizer的内部类ConditionObject中可以看到。

ConditionObject有两个属性firstWaiter和lastWaiter,分别指向的是这个条件对象等待队列的头和尾。

队列的各个节点都是Node(AbstractQueuedSynchronizer的内部类)对象,通过Node对象的nextWaiter之间进行向下传递,所以,条件对象的等待队列是一个单向链表。

下面是await的源代码

public final void await () throws InterruptedException
{

if (Thread.interrupted())

throw new InterruptedException();

Node node = addConditionWaiter();

int savedState
= fullyRelease(node);

int interruptMode
= 0;

while (!isOnSyncQueue(node))
{

LockSupport. park(this);

if ((interruptMode
= checkInterruptWhileWaiting(node)) != 0)

break;

}

if (acquireQueued(node,
savedState) && interruptMode != THROW_IE)

interruptMode = REINTERRUPT;

if (node.nextWaiter != null)

unlinkCancelledWaiters();

if (interruptMode
!= 0)

reportInterruptAfterWait(interruptMode);

}

首先是调用addConditionWaiter把当前线程加入到条件对象的等待队列中,然后fullyRelease来释放锁,然后通过isOnSyncQueue来检查当前线程节点是否在锁对象的等待队列中。

为什么要做这个检查?因为线程被signal唤醒的时候,是首先加入到锁对象的等待队列中的。如果没有在锁对象的等待队列中,那么说明事件还没有发生(也就是没有signal方法没有被调用),所以线程需要阻塞来等待被唤醒。

在addConditionWaiter方法中完成了等待队列的构建过程,代码如下

private Node addConditionWaiter()
{

Node t = lastWaiter;

// If lastWaiter is cancelled, clean out.

if (t
!= null && t.waitStatus !=
Node. CONDITION) {

unlinkCancelledWaiters();

t = lastWaiter;

}

Node node = new Node(Thread.currentThread(),
Node. CONDITION);

if (t
== null )

firstWaiter =
node;

else

t. nextWaiter =
node;

lastWaiter =
node;

return node;

}

线程加入队列的顺序与加入的时间一致,刚加入的线程是在队列的最后面。

下面来看线程的唤醒

public final void signal()
{

if (!isHeldExclusively())

throw new IllegalMonitorStateException();

Node first = firstWaiter;

if (first
!= null)

doSignal(first);

}

唤醒操作实际上是通过doSignal完成,注意这里传递的是firstWaiter指向的节点,也就是唤醒的时候,是从队列头开始唤醒的。

从尾部进入,从头部唤醒,所以这里的等待队列是一个FIFO队列。

private void doSignal (Node
first) {

do {

if (
(firstWaiter = first.nextWaiter)
== null)

lastWaiter = null ;

first. nextWaiter = null ;

while (!transferForSignal(first)
&&

(first = firstWaiter)
!= null );

}

doSignal方法把第一个节点从条件对象的等待队列中移除,然后最终是走到transferForSignal中来进行操作。

final boolean transferForSignal (Node
node) {

/*

* If cannot change waitStatus, the node has been cancelled.

*/

if (!compareAndSetWaitStatus(node,
Node. CONDITION, 0))

return false ;

/*

* Splice onto queue and try to set waitStatus of predecessor to

* indicate that thread is (probably) waiting. If cancelled or

* attempt to set waitStatus fails, wake up to resync (in which

* case the waitStatus can be transiently and harmlessly wrong).

*/

Node p = enq(node);

int ws
= p.waitStatus ;

if (ws
> 0 || !compareAndSetWaitStatus(p, ws, Node. SIGNAL))

LockSupport. unpark(node.thread);

return true ;

}

通过enq方法,把线程所在的节点加入到锁对象的等待队列中,这样在条件合适的时候,线程被唤醒,获得锁,然后执行。

时间: 2024-08-29 19:18:41

条件锁的相关文章

条件锁类

#include <pthread.h> class CTestLock { public: CTestLock() { pthread_mutex_init(&mutex_t_, NULL); pthread_cond_init(&cond_t_, NULL); } ~CTestLock() { pthread_mutex_destroy(&mutex_t_); pthread_cond_destroy(&cond_t_); } int Lock() { in

递归锁+条件锁+互斥锁-04-多线程

1 // 2 // ViewController.m 3 // 05-递归锁(recursive)+条件锁(condition) 4 // 5 // Created by mac on 16/4/20. 6 // Copyright © 2016年 mac. All rights reserved. 7 // 8 /* 9 10 3). 互斥锁 11 NSLock *_lock; 12 13 3)NSLock :不能多次调用,会产生死锁 14 15 2016-04-20 16:06:44.600

NSConditionLock 条件锁

有时候不是简单的需要 加锁/解锁, 而是需要根据一定条件满足后进行 加锁/解锁. 以一个生产中与消费者的例子,介绍条件锁的用法. static NSInteger CONDITION_NO_DATA //条件一: 没有数据 static NSInteger CONDITION_HAS_DATA //条件二: 有数据 //初始化锁时,指定一个默认的条件 NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:CONDIT

条件锁condition与Queue()

在学习之前你应该先了解锁和队列基础 import queue import time import random import threading import asyncio import logging # from queue import Empty logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(levelname)s -->%(funcName)s at line %(lineno)d: \n %(

Golang sync.NewCond条件锁的用法

package main import ( "fmt" "sync" "time" ) func main() { c := sync.NewCond(&sync.Mutex{}) queue := make([]interface{}, 0, 10) removeFromQueue := func(delay time.Duration) { time.Sleep(delay) c.L.Lock() queue = queue[1:]

互斥锁和条件变量的结合使用

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定.而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用.使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化.一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程.这些线程将重新锁定互斥锁并重新测试条件是否满足.一般说来,条件变量被用来进行线承间的同步. 对于条件锁,通常配合一个互斥锁一起使用,以防止多个线程同时请求pt

互斥与锁和条件变量组合

互斥锁一个明显的缺点是,他只有两个状态:锁定和非锁定.而条件变量通过同意线程堵塞和等待还有一个线程发送信号的方法弥补了相互排斥锁的不足,他常和相互排斥锁一起使用. 使用时,条件变量被用来堵塞一个线程,当条件不满足时.线程往往解开对应的相互排斥锁并等待条件发生变化.一旦其它的某个线程改变了条件变量.他将通知对应的条件变量唤醒一个或多个正被此条件变量堵塞的线程.这些线程将又一次锁定相互排斥锁并又一次測试条件是否满足.一般说来,条件变量被用来进行线承间的同步. 对于条件锁,通常配合一个相互排斥锁一起使

《GCD 实现同步锁》-07-多线程

@MicroCai 2015-03-03 23:18 字数 6539 阅读 202 Effective Objective-C Notes:GCD 实现同步锁 Archives iOS <Effective Objective-C Notes>系列博文整理自<Effective Objective-C 2.0> 如果您觉得我的博客对您有帮助,请通过关注我的新浪微博  MicroCai 支持我,谢谢! 本文名为<GCD 实现同步锁>,内容不止于锁.文章试图通过 GCD 同

ios 下锁使用- 09-多线程

ios 下锁使用 时间:2014-08-08 15:36:56 总结下我所了解的ios下的锁: 1. 互斥锁 @synchronized,@synchronized块隐式的添加一个异常处理例程来保护代码.该处理例程会在异常抛出的时候自动的释放互斥锁.这意味着为了使用@synchronized指令,你必须在你的代码中启用异常处理. NSLock NSRecursiveLock,递归锁,一个线程中可以多次使用 NSConditionLock,条件锁,其实也是一种互斥锁:类似的有NSCondition