Java并发编程中构建自定义同步工具
当java类库没有提供适合的同步工具时,就需要构建自定义同步工具。
可阻塞状态依赖操作的结构
acquir lock on object state;//请求获取锁
while(precondition does not hold){//没有满足前提条件
release lock;//先释放锁
wait until precondition might hold;//等待满足前提条件
optionlly fail if interrupted or timeout expires;//因为中断或者超时执行失败
reacquire lock;//重新尝试获取锁
}
perform action//执行
release lock;//释放锁
有界缓存实现基类示例
public class baseboundbuffer<v> {
private final v[] buf;
private int tail;
private int head;
private int count;
@suppresswarnings("unchecked")
public baseboundbuffer(int capacity) {
buf = (v[]) new object[capacity];
}
public synchronized void doput(v v) {
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
count++;
}
public synchronized v dotake() {
v v = buf[head];
if (++head == buf.length)
head = 0;
count--;
return v;
}
public final synchronized boolean isfull() {
return count == buf.length;
}
public final synchronized boolean isempty() {
return count == 0;
}
}
阻塞实现方式一:抛异常给调用者
public synchronized void put1(v v) throws exception{
if(isfull())
throw new exception("full error");
doput(v);
}
分析:异常应该应用于发生异常情况中,在这里抛异常不合适;需要调用者是处理前提条件失败的情况,并没有解决根本问题。
阻塞实现方式二:通过轮询和休眠
public void put2(v v) throws interruptedexception {
while (true) {//轮询
synchronized (this) {
if (!isfull()) {
doput(v);
return;
}
}
thread.sleep(sleep_time);//休眠
}
}
分析:很难权衡休眠时间sleep_time设置。如果设置过小,cpu可能会轮询多次,消耗cpu资源也越高;如果设置过大,响应性就越低。
阻塞实现方式三:条件队列
条件队列中的元素是一个个等待相关条件的线程。每个java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且object中的wait、notify、notifyall方法就构成了内部条件队列的api。object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其它线程能获得这个锁并修改对象的状态。object.notify和object.notifyall能唤醒正在等待线程,从条件队列中选取一个线程唤醒并尝试重新获取锁。
public synchronized void put3(v v) throws interruptedexception {
while(isfull())
wait();
doput(v);
notifyall();
}
分析:获得较好响应,简单易用。
使用条件队列
1.条件谓词
1).定义:条件谓词是使某个操作成为状态依赖操作的前提条件。条件谓词是由类中各个状态变量构成的表达式。例如,对于put方法的条件谓词就是“缓存不为空”。
2).关系:在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而每个状态变量必须由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(及调用wait和notify等方法所在的对象)必须是同一个对象。
3).约束:每次调用wait都会隐式地和特定的条件谓词相关联,当调用特定条件谓词时,调用者必须已经持有与条件队列相关的锁,这个锁必须还保护这组成条件谓词的状态变量
2.条件队列使用规则
1).通常都有一个条件谓词
2).永远在调用wait之前测试条件谓词,并且在wait中返回后再次测试;
3).永远在循环中调用wait;
4).确保构成条件谓词的状态变量被锁保护,而这个锁必须与这个条件队列相关联;
5).当调用wait、notify和notifyall时,要持有与条件队列相关联的锁;
6).在检查条件谓词之后,开始执行被保护的逻辑之前,不要释放锁;
3.通知
尽量使用notifyall,而不是nofify.因为nofify会随机唤醒一个线程从休眠状态变为blocked状态(blocked状态是种线程一直处于尝试获取锁的状态,即一旦发现锁可用,马上持有锁),而notifyall会唤醒条件队列中所有的线程从休眠状态变为blocked状态.考虑这么种情况,假如线程a因为条件谓词pa进入休眠状态,线程b因为条件谓词pb进入休眠状态.这时pb为真,线程c执行单一的notify.如果jvm随机选择了线程a进行唤醒,那么线程a检查条件谓词pa不为真后又进入了休眠状态.从这以后再也没有其它线程能被唤醒,程序会一直处于休眠状态.如果使用notifyall就不一样了,jvm会唤醒条件队列中所有等待线程从休眠状态变为blocked状态,即使随机选出一个线程一因为条件谓词不为真进入休眠状态,其它线程也会去竞争锁从而继续执行下去.
4.状态依赖方法的标准形式
void statedependentmethod throwsinterruptedexception{
synchronized(lock){
while(!conditionpredicate))
lock.wait();
}
//dosomething();
....
notifyall();
}
显示condition对象
显示的condition对象是一种更灵活的选择,提供了更丰富的功能:在每个锁上可以存在多个等待,条件等待可以是中断的获不可中断的,基于时限的等待,以及公平的或非公平的队列操作。一个condition可以和一个lock关联起来,就像一个条件队列和一个内置锁关联起来一样。要创建一个condition,可以在相关联的lock上调用lock.newcondition方法。以下用显示条件变量重新实现有界缓存
public class conditionboundedbuffer<v> {
private final v[] buf;
private int tail;
private int head;
private int count;
private lock lock = new reentrantlock();
private condition notfullcondition = lock.newcondition();
private condition notemptycondition = lock.newcondition();
@suppresswarnings("unchecked")
public conditionboundedbuffer(int capacity) {
buf = (v[]) new object[capacity];
}
public void doput(v v) throws interruptedexception {
try {
lock.lock();
while (count == buf.length)
notfullcondition.await();
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
count++;
notemptycondition.signal();
} finally {
lock.unlock();
}
}
public v dotake() throws interruptedexception {
try {
lock.lock();
while (count == 0)
notemptycondition.await();
v v = buf[head];
buf[head] = null;
if (++head == buf.length)
head = 0;
count--;
notfullcondition.signal();
return v;
} finally {
lock.unlock();
}
}
}
上一篇: SpringBoot任务调度器的实现代码
下一篇: SpringBoot框架搭建