版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/goldenfish1919/article/details/80936399
jdk5之前线程同步可以用synchronized/wait/notify来进行控制,jdk5以后新添加了lock/condition。他们之间有什么联系与区别的?本文就用一个例子循序渐进的给大家展示一下:
首先来看一个有界缓存的例子:
abstract class BaseBoundedBuffer<V> {
private final V[] buff;
private int tail;
private int head;
private int count;
protected BaseBoundedBuffer(int capacity){
this.buff = (V[])new Object[capacity];
}
protected synchronized final void doPut(V v){//存
buff[tail] = v;
tail++;
if(tail == buff.length){
tail = 0;
}
count++;
}
protected synchronized final V doTake(){//get
V v = buff[head];
buff[head] = null;
head++;
if(head == buff.length){
head = 0;
}
count--;
return v;
}
protected synchronized final boolean isFull(){//是否是满的
return count == buff.length;
}
protected synchronized final boolean isEmpty(){//是否是空的
return count == 0;
}
}
class GrumpBoundedBufer<V> extends BaseBoundedBuffer<V>{
public GrumpBoundedBufer(int size){
super(size);
}
public synchronized void put(V v)throws BufferFullException{
if(isFull()){//存的时候,如果是满的,就抛异常
throw new BufferFullException();
}
doPut(v);
}
public synchronized V take()throws BufferEmptyException{
if(isEmpty()){//取的时候,如果是空的,就抛异常
throw new BufferEmptyException();
}
return doTake();
}
}
当然,上面的这种实现非常不友好,如果不满足先验条件就抛出异常,但是在多线程条件下,先验条件不会保持一个一成不变的状态,队列里面的元素是在不停的变化的,因此我们用轮询加休眠改进一下:
class SleepyBoundedBufer<V> extends BaseBoundedBuffer<V>{
public SleepyBoundedBufer(int size){
super(size);
}
public void put(V v) throws InterruptedException {
while(true){
synchronized(this){
if(!isFull()){//如果不是满的,可以存
doPut(v);
return;
}
}
//如果是满的,休眠1秒钟,然后重试
Thread.sleep(1000);
}
}
public V take() throws InterruptedException {
while(true){
synchronized(this){
if(!isEmpty()){//如果不是空的,就可以取
return doTake();
}
}
//如果是空的,休眠1秒钟,重试
Thread.sleep(1000);
}
}
}
这种轮训+休眠的方式的缺点:
(1)休眠多少时间合适呢?
(2)给调用者提出处理InterruptedException的新的要求,因为sleep是会抛出这个异常的。
如果存在一种线程挂起的方式,它能保证,在某个条件变为真的时候,线程可以及时的苏醒过来,那就太好了!这就是条件队列所做的事情。
使用内部条件队列的实现方式:
class BoundedBufer<V> extends BaseBoundedBuffer<V>{
protected BoundedBufer(int size) {
super(size);
}
public synchronized void put(V v) throws InterruptedException {
while(isFull()){//注意这里的while,而不是if
wait();//如果是满的,把当前线程挂起
}
doPut(v);//如果不满,就可以存
notifyAll();//存了以后,唤醒所有的等待线程,因为可能有线程在等待取,放进来以后就可以取了
}
public synchronized V take() throws InterruptedException {
while(isEmpty()){//注意这里的while,而不是if
wait();//如果是空的,把当前线程挂起
}
V v = doTake();//如果不空,取出来
notifyAll();//然后唤醒所有的等待线程,因为有的线程可能在等待放,取出来以后就可以放了
return v;
}
}
这也是jdk5之前的解决方式。
条件队列可以让一组线程(叫做:等待集wait set)以某种方式等待相关条件变为真,条件队列的元素不同于一般的队列,一般队列的元素是数据项,条件队列的元素是线程。每个java对象都有一个内部锁,同时还有一个内部条件队列。一个对象的内部锁和内部条件队列是关联在一块的。Object.wait会自动释放锁,并请求os挂起当前线程,这样就给其他线程获得锁并修改对象状态的机会,当线程被唤醒以后,它会重新去获取锁。调用wait以后,线程就进入了对象的内部条件队列里面等待,调用notify以后,就从对象的内部条件队列里面选择一个等待线程,唤醒。 因为会有多个线程因为不同的原因在同一个条件队列中等待,因此,用notify而不用notifyAll是危险的!有的线程是在take()的时候阻塞,它等待的条件是队列不空,有的线程是在put()的时候阻塞,它等待的条件是队列非满。 如果调用了take()以后notify的是总是阻塞在take上的线程,就挂了!
BoundedBufer的put和take是一种很保守的做法,每次向队列里面添加或者移除都进行notifyAll,可以进行如下的优化:
是有从空变为了非空,或者是从满变为了不满的时候,才需要从条件队列里面唤醒一个线程。
class ConditionalBoundedBufer<V> extends BaseBoundedBuffer<V>{
protected ConditionalBoundedBufer(int size) {
super(size);
}
public synchronized void put(V v) throws InterruptedException {
while(isFull()){
wait();
}
boolean isEmpty = isEmpty();
doPut(v);
if(isEmpty){//从空变为了非空的时候,才需要唤醒(而实际上需要唤醒那些take线程,而不是put线程)
notifyAll();
}
}
public synchronized V take() throws InterruptedException {
while(isEmpty()){
wait();
}
boolean isFull = isFull();
V v = doTake();
if(isFull){//从满变为了不满,才需要唤醒(而实际上需要唤醒那些put线程,而不是take线程)
notifyAll();
}
return v;
}
}
这只是一种小技巧,会加大程序的复杂性,不提倡!
从空变为了非空,唤醒的应该是那些阻塞在take()上的,从满变为了不满唤醒的应该是那些阻塞在put()上的线程,而notifyAll会把所有条件队列里面的所有的等待的线程全部唤醒,这就显现出了内部条件队列有一个缺陷:内部锁只能有一个与之关联的条件队列。显式的condition的出现就是为了解决这个问题。
正如Lock提供了比内部锁更丰富的特征一样,condition也提供了比内部条件队列更丰富更灵活的功能。一个lock可以有多个condition,一个condition只关联到一个Lock。
class ConditionBoundedBufer<T> {//使用显式的条件变量,HLL的登场了
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
private final T[] items = (T[])new Object[100];
private int head,tail,count;
//阻塞,一直到notFull
public void put(T t) throws InterruptedException {
lock.lock();
try{
while(count == items.length){
notFull.await();//等待非满
}
items[tail] = t;
tail ++;
if(tail == items.length){
tail = 0;
}
count++;
notEmpty.signal();//唤醒那些执行take()而阻塞的线程
}finally{
lock.unlock();
}
}
//阻塞,一直到notEmpty
public T take() throws InterruptedException {
lock.lock();
try{
while(count == 0){
notEmpty.await();//等待非空
}
T t = items[head];
items[head] = null;
head ++;
if(head == items.length){
head = 0;
}
count--;
notFull.signal();//唤醒那些执行put()而阻塞的线程
return t;
}finally{
lock.unlock();
}
}
}
至此,上面的所有的问题已经全部完美的得到了解决!
希望以上对你理解wait¬ify,lock&condition有所帮助,也欢迎大家观看我的两个视频课程:
Java生产环境下性能监控与调优详解 https://coding.imooc.com/class/241.html
Java秒杀系统方案优化 高性能高并发实战 https://coding.imooc.com/class/168.html