Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

多线程学习(四)之高级特性

2017-06-03
nivelle

显式锁

ReentrantLock并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。

与内置锁不同,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、属性保证以及性能特性等方面可以有所不同。

public interface Lock{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(lon timeout,TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
    
}

ReentantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。

与synchronized一样,ReentrantLock还提供了可重入的加锁语义。

内置锁必须必须在获取该锁的代码中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。

Lock接口的标准使用形式:必须在finally块中释放锁。否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。当使用加锁时,还必须考虑在try块中抛出异常的情况,如果可能使对象处于某种不一致的状态,那么就需要更多的try-catch或try-finall代码块。

 Lock locl = new ReentrantLock();
 ...
 lock.lock();
 try{
   //更新对象状态
   // 捕获异常,并在必要时恢复不变性条件
 }finally{
   lock.unlock();
 }

轮询锁与定时锁

可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。在内置锁中,死锁时一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁的锁提供了另一种选择:避免死锁的发生。

如果不能获得所有需要的锁,那么可以使用可定时或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。

读写锁

ReentrantLock实现了一种标准的互斥锁:每次最多只能有一个线程能持有ReentrantLock.但对于维护数据完整性来说,互斥通常是一种过于强硬的加锁规则,因此也就不必要地限制了并发性.

ReadWriteLock接口中暴露了两个Lock对象,其中一个用于读操作,而另一个用于写操作。要读取由ReadWriteLock保护的数据,必须首先获获得读取锁,当需要修改ReadWriteLock保护的数据时,必须首先获得写入锁。尽管这两个锁看上去是相互独立的,但读取锁和写入锁只是读————写锁对象的不同视图。

public interface ReadWriteLock{
  Lock readLock();
  Lock writeLock();
}

在读——写锁实现的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。

ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义.与ReentrantLock类似,ReentrantReadWriteLock在构造时页可以选择是一个非公平的锁还是一个公平的锁.在公平锁中,等待时间最长的线程将优先获得锁,如果这个锁有读线程持有,而另一个线程请求写入,那么其他读线程都不能获得读取锁,直到写线程使用并释放了写入锁.在非公平锁中,线程获得访问许可的顺序是不确定的.写线程降级为读线程是可以的,但从读线程升级为写则是不可以的.(这样做会导致死锁).

与ReentrantLock类似的是,ReentrantReadWriteLock中的写入锁只能有唯一的所有者,并且只能由获得该锁的线程来释放.

当锁的持有时间较长并且大部分都不会修改呗守护资源时,那么读-写锁能提供并发性.

条件队列

它使得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变成真。传统队列的元素是一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。

条件谓词

条件谓词是使某个操作成为状态依赖操作的前提条件.

在条件等待中存在一种重要的三元关系,包括加锁,wait方法和一个条件谓词.在条件谓词中包含多个状态变量.而状态变量由一个锁来保护,因此,在测试条件谓词之前必须先持有这个锁.锁对象与条件队列对象(调用wait和notify等方法所在兑现)必须是同一个对象.

例如:

在BoundedBuffer中,缓存的状态是由缓存锁保护的,并且缓存对象被用作条件队列.take方法将获取请求缓存锁,然后对条件谓词进行测试.如果缓存非空,那么它会移除第一个元素.之所以能这样做,是因为take此时任然保持有保护缓存状态的锁.

如果条件谓词不为真(缓存为空),那么take必须等待并直到另一个线程在缓存中放入一个对象.take将在缓存的内置条件队列上调用wait方法,这需要持有条件队列上的锁.wait方法将释放锁,阻塞当前线程,并等待直到超时,然后线程呗中断或者通过一个通知被唤醒.在唤醒进程后,wait在返回前还要重新获取锁.当线程从wai方法中被唤醒时,他在重新请求锁时不具有任何特殊的优先级,而要与任何其他尝试进入同步代码块的线程一起正常地在锁上竞争.


过早唤醒

内置条件队列可以与多个条件谓词一起使用.当一个线程由于调用notifyAll而醒来时,并不意味着该线程正在等待的条件谓词已经变成真的了.另外,wait方法还可以”假装”返回,而不是由于某个线程调用了notify.

正如每个Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait、notify和notifyAll方法就构成了内部条件队列的API.对象的内置锁与其内部条件队列是相互关联的,要调用对象X中条件队列的任何一个方法,必须持有对象X上的锁。这是因为“等待由状态构成的条件”与“维护状态一致性”这两种机制必须被紧密地绑定在一起:只能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程。

Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使得其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获取锁。从直观上来理解,调用wait意味着“我要去休息了,但当发生特定的事情时唤醒我”,而调用通知方法就意味着“特定的事情发生了”。

基于这个原因,每当线程从wait中唤醒时,都必须再次测试条件谓词,如果条件谓词不为真,那么就继续等待(或者失败).由于线程在条件谓词不为真的情况下可以反复地醒来,因此必须在一个循环中调用wait,并在每次迭代中都测试条件谓词.


void stateDependentMethod() throws InterruptedException{
    
    //必须通过一个锁来保护条件谓词
    
    synchronized(lock){
        while(!conditionPredicate){
            lock.wait();
            //现在对象处于合适的状态
        }
    }
}


当使用条件等待时(如Obejct.wait或Condition.await):

  • 通常都有一个条件谓词:包括一些对象状态的测试,线程在执行前必须首先通过这些测试
  • 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试
  • 在一个循环中调用wait
  • 确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量
  • 当调用wait,notify,notifyAll等方法时,一定要持有与条件队列相关的锁
  • 在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁.
通知

每当在等待一个条件时,一定要确保在条件谓词变为真时通过某种方式发出通知.在条件队列API中有两个发出通知的方法,即notify和notifyAll.无论调用哪一个,都必须持有与条件队列对象相关联的锁.由于在调用notify和notifyAll时必须持有条件队列对象的锁,而如果这些等待中的线程此时不能重新获得锁,那么无法从wait返回,因此发出通知的线程应该尽快地释放,从而确保正在等待的线程尽可能快递解除阻塞.

只有同时满足以下条件才使用notify:

  • 所有等待线程的类型都相同.只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作.
  • 单进单出.在条件变量上的每次通知,最多只能唤醒一个线程来执行.

显式的Condition对象

condition是一种广义的内置条件队列.

public interface Condition{
    void await() throws InterruptedException;
    boolean await(long time,TimeUnit unit) thorws InterruptedException;
    long awaitNaons(long nanosTimeout) throws InterruptedException;
    void awaitUninterruptibly();
    boolean awaitUntil(Date dealine)throws InterruptedException;
    void signal();
    void signaAll();
}

内置条件队列存在一些缺陷.每个内置锁都只能有一个相关联的条件队列,因而在像boundedBuffer这种队列,多个线程可能在同一个条件队列上等待不同的条件谓词,并且在最常见的加锁模式下公开条件队列对象.这些因素都使得无法满足在使用notifyAll时所有等待线程未同一类型的需求.如果想编写一个带有多个条件谓词的并发对象,或者想获得除了条件队列可见性之外的更多控制权,就可以使用显示的Lock和Condition而不是内置锁和条件队列,这是一张更灵活的选择.

一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样.要创建一个Conditon,可以在相关联的Lock上调用Lock.newCondition方法.Condition比内置条件队列提供了更加丰富的功能:每个锁上存在多个等待,条件等待可以是中断的或不可中断的,基于时限的等待,以及公平的或非公平的队列操作.

与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象.Conditio对象继承了相关的Lock对象的公平性,对于公平的锁,线程会依照FIFO顺序从Condition.await中释放.

在Condition对象中,与wait,notify和notifyAll方法对应的分别是await,signal和signalAll.但是,Condition对Object进行
了扩展,因而它也包含wait和notify方法.


评论