java并发编程实践学习(14 ) 构建自定义的同步工具

##一.管理状态依赖性

在单线程化的程序中,如果调用一个方法时,依赖于状态的先验条件为满足(比如连接池非空),那么这个先验条件就无法变为真。但是在并发程序中,基于状态的先验条件会在其他线程的活动中被改变。对于并发对象,依赖于状态的方法有时可以再不能满足先验条件的情况下选择失败,不过更好的选择是等待先验条件变为真。
有一种依赖于状态的操作,能够被阻塞直到可以继续执行。内部条件队列的机制可以让线程一直阻塞,指导对象已经进入某个特定的状态,该状态下进程可以继续执行,并且在阻塞线程可以进行进一步执行的时候,对象会将它们唤醒。“轮训与休眠”来解决状态依赖操作(费力不讨好)。

1.将先验条件失败传给调用者

当先验条件不满足时,调用者可以不用休眠而是直接重试操作-这种被称作忙等待或者自旋等待。如果在相当长的一段时间内,缓存的状态都不会改变,那么使用这种方法就会消耗相当多的CPU时间。另一个方面,调用者可以决定休眠,以避免消耗过多的CPU时间,但是如果缓存的状态在休眠不久的时候很快发生了变化,那么他很容易睡过头每次迭代中,在等待与休眠之间一种折中的选择是调用Thread.yield,这给调度器一个提示:我现在可以让出一定的时间让另外的线程运行。

2.利用“轮询加休眠”实现拙劣的阻塞

轮训和休眠重试机制为每次调用实现了重试逻辑,从而分担调用者的麻烦。如果缓存是空的take将休眠,直到另一个线程在缓存中置入了一些数据,
有限缓存使用了拙劣的阻塞

@ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V>{
    public SleepyBoundedBuffer(int siz){
        super(size);
    }
    public void put(V v) throws InterruptedException{
        while(true){
            synchronized (this){
                if(!isFull){
                    doPut(v);
                    return;
                }
            }
        }
    }

    public V take() throws InterruptedException{
        while(true){
            sychronized (this){
                if(!isEmpty){
                    return doTake();
                }
            }
        }
    }
}

然而这样讲轮训和休眠组合成一个阻塞操作的尝试都不能满意。

3.让条件队列来解决这一切

条件队列可以让一组线程—称作工作集以某种方式等待相关条件变成真。不同于传统队列,它们的元素是数据项;条件队列的元素是等待相关条件的线程。
有限缓存使用条件队列

@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V>{
    //条件谓词:not-full(!isFull)
    //条件谓词:not-empty(!isEmpty)
    public BoundedBuffer(int size){
        super(size);    
    }

    public synchronized void put(V v)throws InterruptedException{
        while(isFull())
            wait();
        doPut(v);
        notifyAll();
    }

    public synchronized V take()throws InterruptedException{
        while(isEmpty)
            wait();
        V v = doTake();
        notifyAll();
        return v;
    }
}

BoundedBuffered目前已经足够好了——简单够用,而且把状态独立性管理的非常清晰。

二.使用条件队列

条件队列让构建有效且可响应的状态依赖变得容易。但是将它们用错也很容易。

1.条件谓词

正确使用条件队列的关键在于识别出对象可以等待的条件谓词。条件谓词是先验条件的第一站,它在一个操作与状态只建立起依赖关系。

将条件谓词和与之关联的条件队列,以及在队列中等待的操作,都写入文档。

2.过早的唤醒

一个单独的内部条件队列可以与多个条件谓词共同使用。
当有人调用notifyAll,唤醒了你的线程的时候,并不意味着你正在等待的条件谓词变成真了。所以当你从wait中唤醒后,都必须再次测试条件谓词,如果条件谓词尚未成真,就继续等待或失败。

当使用条件等待时:

  • 永远设置一个条件谓词——一些对象状态的测试,线程执行前必须满足它。
  • 永远在调用wait前测试条件谓词,并且从wait中返回后再次测试。
  • 永远在循环中调用wait
  • 确保构成条件谓词的状态变量被锁保护,而这个锁正是与条件队列相关联的。
  • 当调用wait、notify或者notifyAll时,要持有与条件队列相关量的锁
  • 在检查条件谓词之后、开始执行被保护的逻辑之前不要释放锁。

3.丢失的信号

死锁和活锁可以导致活跃度失败,另一种形式的活跃度失败是丢失的信号。当一个线程等待的条件已经为真,但是进入等待前的条件谓词却返回了假,我们称这样就出现了一个丢失的信号。

4.通知

无论何时当你在等待一个条件时,一定要确保有人会在条件谓词变为真时通知你。
在条件队列中有两个通知方法——notify和notifyAll。无论调用哪一个你都必须持有与条件队列相关联的锁。调用notify的结果是:JVM会在这个条件队列中等待的众多线程中挑选出一个,并把它唤醒。而你调用notifyAll会唤醒所有正在这个条件队列中等待的线程。

只有同时满足下述条件后才能使用单一的notify取代notifyAll
相同的等待者。只有一个条件谓词与条件队列相关,每个线程,从wait返回后执行相同的逻辑
一进一出,一个对条件变量的通知,至多只激活一个线程执行。

5.子类安全问题

一个依赖于状态的类,要么完全将它的等待和通知协议暴露(并文档化给子类),要我们完全阻止子类参与其中。

6.封装条件队列

通常最好可以把条件队列封装起来,这样在使用它的类层次结构之外,是不能访问它的。

7.入口协议和出口协议

Wellings用“入口协议和出口协议”的形式刻画了wait和notify的正确使用方法。对于每个依赖于状态的操作,以及每个修改了其他状态的操作,你都应该为其定义并文档化一个入口协议和出口协议。入口协议就是操作的条件谓词,出口协议涉及到要检查任何备操作改变的状态变量,确认它们是否引起其他一些条件谓词变为真,如果是,通知相关的条件队列。

三、显示的Condition对象

Condition是广义的内部条件队列。内部条件队列有一些缺陷,每个内部锁只能有一个与之相关的条件队列。这意味着多个线程可能为了不同的条件谓词在同一个条件队列中等待,而且大多数常见的显示锁都会暴露条件队列对象。

危险警告:wait、notify和notifyAll在Condition中的对等体是await、signal和signalAll.但是Condition继承于Object所以它也有wait和notify,一定要使用正确-await和signal

四.剖析Synchronizer

Reentrant和Semaphore这俩个接口有很多共同点。这些类都扮演了“阀门”的角色,每次只允许优先数目的线程通过它;线程到达阀门后,可以通过(lock或acquire成功返回),可以等待(lock或acquire阻塞),也可以被取消(tryLock或tryAcquire返回false,指明在允许的时间内,锁或者“许可”不可用), 更进一步它们都允许可中断的,不可中断的,可限时的请求尝试,它们也都允许选择公平、非公平的等待线程队列。
事实上它们的实现都用到共同的基类,AbstractQueuedSynchronizer(AQS),它是一个用来构建锁和Synchronizer的框架

五、AbstractQueuedSynchronized

一个基于AQS的Synchronizer所执行的基本操作,是一些不同形式的获取和释放。获取操作是状态依赖的操作,总能够阻塞。借助锁和信号量。“获取”的含义变得相当直观—获取锁或者许可—并且调用者可能不得不去等待,直到Synchronizer处于可发生的状态。CountDownLatch的请求意味着“等待,直到闭锁到达它的终止状态“FutureTask意味着“等待,直到任务已经完成”。“释放”不是一个可阻塞的操作;“释放”可以允许线程在请求执行前阻塞。
支持独占获取的Synchronizer应该实现tryAcquire、tryRelease和isHeldExclusively这几个受保护的方法,而支持共享获取的Synchronizer应该实现tryAcquireShared。

1.一个简单的闭锁

二元闭锁使用

@ThreadSafe
public class OneShotLatch{
    private final Sync sync = new Sync();
    public void signal(){
        sync.releaseShared(0);
    }

    public void await()throws InterruptedException{
        sync.acquireSharedInterruptibly(0);
    }

    private class Sync extends AbstractQueueSynchronizer{
        //如果闭锁打开成功(state==1)否则失败
        return (getState()==1?1:-1;
    }

    protected boolean tryReleaseShared(int ignored){
        setState(1);//闭锁已经打开
        return true;//现在其他线程可以获得闭锁
    }
}

6、java.util.concurrent的Synchronizer类中的AQS

1.ReentrantLock

ReentrantLock只支持独占的获取操作,因此他实现了tryAcquire、tryRelease和isHeldExclusively。ReentrantLock使用同步状态持有锁获取操作的计算,还维护一个owner变量来持有的线程标识符。
当一个线程尝试去获取锁,tryAcquire会首先请求锁的状态。如果锁未被占用,它会尝试更新锁的状态,表明锁已被占有。它还利用AQS内置的对多条件变量和多等待集的支持。###2.Semaphore和CountDownLatch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值