您的位置:首页 > 编程语言 > Java开发

《java7核心技术与最佳实践》读书笔记之 multi-thread (3)

2014-03-24 23:28 567 查看
在java平台出现很长一段时间内,开发多thread程序只能使用java平台提供的synchronized和volatile关键字,以及object类中的wait,notify和notifyall方法。以上抽象层次比较低,在开发中使用起来比较繁琐,而且容易产生错误。而且线程间交互方式存在某些固定的模式,比如生产者-消费者模式和读-写模式。后来J2SE5.0引入juc包,提供了高层次的API,可以满足日常开发中常见的需求。

高级同步机制 

虽然非阻塞方式性能要优于阻塞方式,但并非所有场景都要采用非阻塞的方式实现,有很多情况下仍然需要使用基于锁机制的阻塞方式实现。juc包的locks接口就是一个锁,可以通过其中的lock方法获取锁,unlock来解锁。使用lock接口的代码需要保证锁总是被释放,一般把unlock方法放在finally代码块中。lock方法获取锁的方式类似于synchronized关键词,以阻塞方式获取锁。另外还可以通过trylock方法以非阻塞方式获取锁。如果在调用trylock方法时无法获取锁,直接返回false,不会阻塞当前thread。利用trylock方法另外一种重载形式可以指定超时时间。如果指定了超时时间,当无法获取锁时,当前thread会阻塞,但等待的时间不会超进指定的超时时间,同时thread也是可以被中断的。

另外一个与锁相关的接口是ReadWriteLock.rwl接口实际上表示的是两个锁,一个是读取操作相关的锁,另一个是写入操作使用的排他锁。该接口适合于解决常见的读取-写入问题类似场景。在没有thread进行写入操作时,进行读取操作的多个thread都可以获取读取锁,而进行写入操作的thread只有在获取写入锁后才能进行写入操作。多个thread可以同时进行读取操作,但是同一时刻只允许一个thread进行写入操作。在大多数情况下,对一个数据结构的读取操作次数要远多于写入操作的次数。

juc。locks包中提供lock接口和readwritelock接口的基本实现,分别为reentrantlock和reentrantreadwritelock类。这两具类共同特征是可重入性,即允许一个thread多次获取同一个锁。reentranlock类对象可以有一个所有者thread,表示上一次成功获取该锁,但还没有释放锁的thread。reentrantlock类的对象同时保存了所有者thread在该对象上加锁的次数。通过getholdcount方法可以获取当前的加锁次数。如果reentrantlock类的对象当前没有所有者thread,则当前thread获取锁的操作会成功,加锁次数为1。在随后的操作中,thread可以再次获取该锁,这也是可重入的含义所在。每次加锁操作会使加锁次数加1,而每一次调用unlock方法释放锁会使加锁次数减1。当加锁次数变为0,该锁会被释放,可以被其他thread获取。

在创建reentrantlock类对象时可以通过一个额外的boolean类型参数来声明使用更加公平的加锁机制。在使用锁机制会遇到一个问题是thread饥饿问题。当多个thread同时竞争某个锁时,可能有的thread一直无法成功获取锁,一直处于无法运行状态。thread饥饿是有些程序应该避免的问题。如果在创建reentrantlock类的对象时添加了额外的参数true,则reentrantlock会使用相对公平的锁分配策略。当锁处于可被获取状态时,在由于尝试获取该锁而处于等待状态的thread中,等待时间最长的thread会成功获取这个锁。这就避免了thread饥饿问题。不带参数的trylock方法会忽略公平模式的设置。

可重入锁优势在于减少了锁在各thread间传递次数,可以提高程序的吞吐量。为了提高程序整体吞吐量应该尽可能使用可重入锁。lock接口代替synchronized,相对应的condition接口替代object类的wait,notify和notifyall方法。使用condition接口时也需要与一个对应的lock接口实现对象关联起来。通过lock接口的newcondition方法可以创建新的condition接口的实现对象。在调用condition接口的方法之前,也需要使用lock接口的方法来获取锁。condition接口提供了多个类似object类的wait方法的方法,最基本的是await方法,调用该方法会使当前thread进行等待状态,直到被唤醒或被中断。另外一种await方法的重载形式可以指定超时时间。方法awaitnanos以纳秒数为单位指定超时时间,该方法返回值是剩余等待时间的估计值。类似的awaituntil方法也可以指定超时时间,只不过指定的不是要经过的时间而是超时发生的时间点,参数是一个java.util.date类的对象。前面几种方法都会响应其他thread发出的中断请求,而awaituninterruptibly方法则不会处理中断请求。

与condition接口中等待方法相对应的是signal和signalall方法,相当于object的notify和notifyall方法,示例

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

lock.lock();

try{

while(logic condition){

condition.await();

}

}finally{

lock.unlock();

}

底层同步器

在一个multi thread中,thread 间可能存在多种不同的同步方式,一种比较常邮的需求是对有限个共享资源的同步访问,multi thread程序中很多场景都可以抽象成这类同步方式。比如对某个监视器对象的互斥访问,实际上是多个thread在竞争唯一的一个资源。如果系统中安装了两台打印机,那么需要进行打印操作的多个thread想互竞争这两个资源。当多个thread在等待同一个资源时,从公平的角度出发,这些thread会被放入到一个先入先出FIFO队列中,当资源变成可用时,处于队首的thread会获取该资源。

如果程序中同步方式可以抽象成对有限个资源的同步访问,可以使用juc。locks包中的abstractqueuesynchronizer类和abstractqueuedlongsynchronizer类作为实现的基础。这两个类的作用是相同的,只不过前者在内部使用一个int类型的变量来维护内部状态,而后者使用一个long类型的变量。可以将这个内部变量理解成共享资源的个数。通过getstate,setstate和compareandset这三个方法来更新这个内部变量的值。它们都是abstract的,因此需要继承并重写其中包含的部分方法后才以使用。通常做法是把它们的子类作为一个java内部类。外部的java类提供具体的同步方式。

public class SimpleResourceManager{
private final InnerSynchronizer synchronizer;
private static class InnerSynchronizer extends AbstractQueuedSynchronizer{
InnerSynchronizer(int numOfResources){
setState(numOfResources);
}
protected int tryAcquireShared(int qcquires){
for(;;){
int available = getState();
int remaining = available - acquires;
if(remaining < 0 || compareAndSetState(available,remaining)){
return remaining;
}
}
}
protected boolean tryReleaseShared(int releases){
for(;;){
int available = getState();
int next = available + releases;
if(compareAndSetState(available,next)){
return true;
}

}
}
//
}
public SimpleResourceManager(int numOfResources){
synchronizer = new InnerSynchronizer(numOfResources);
}
public void acquire() throws InterruptedException {
synchronizer.qcquireSharedInterruptibly(1);
}
public void release(){
synchronizer.releaseShared(1);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: