双缓冲队列

前段时间,做了个“双缓冲队列”,可是测试的效果就是不怎么明显,理论完全都在这里,可是就是看不到效果。

 

昨天在胡总的提示下,终于意识到不该用阻塞队列,换成普通的List对象,这样效果就明显多啦~~

 

又重新写了一篇文档,如下

 

提出问题:为啥要有双缓冲队列?
    引用09年9月《程序员》上的一句话:双缓冲队列就是冲着同步/互斥的开销来的。我们知道,在多个线程并发访问同一个资源的时候,需要特别注意线程的同步问题。稍稍不注意,哦活,程序结果不正确了。最经典的就是“银行取钱”的例子,想想,都跟现金挂上钩了,看来这真不容忽视。
    今天我们要谈的不是如何去给资源加锁解锁来解决同步问题,今天的重点在于,如何将线程同步的开销降低到我们力所能及的程度。如果你觉得,你可以通过增加硬件资源来弥补程序开销,那么,你将不可能成为一个优秀的程序员。
    进入正题,先引入一个例子,两个实体:一个是玩具工厂,它的工作就是不停地生产玩具;另外一个实体就是小孩,它的工作就是不停地从工厂拿玩具。小孩不可能直接到工厂去“拿”玩具吧?呵呵,妈妈是绝对不会放心的。所以,我们有一个“搬运工”,搬运工自然要具备“存放”的功能,不然他怎么将玩具带给小孩呢,是吧。所以,我们先将搬运工定义为一个List,用来存放工厂生产出来的玩具。
代码如下
玩具类,定义一个玩具实体
public class Toy {
    private String name;

    public String getName() {
        return name;
    }
public void setName(String name) {
        this.name = name;
    }
}

接下来是玩具工厂,它得“不停地”生产,所以,把它设计成一个线程类

玩具工厂,将玩具对象,放到list中
public class Factory extends Thread{
    

    public void run(){
        while(true){
        
        Toy t = new Toy ();
        
        t.setName("玩具");
        synchronized (Tools.lT){
            Tools.lT.add(t);
}
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
         }
    }
}
注意到,在上面这段代码当中,必须得用synchronized (Tools.lT)将Tools.lT给加锁。不然会出现线程同步问题(开销就在这里)。

再接下来,看看小孩是怎么“玩”玩具的:
小孩类,从list中取出玩具对象
public class Kid extends Thread {
    long time1 = System.currentTimeMillis();
    int count = 0;
    public void run() {
        while(true){
        
            synchronized(Tools.lT){
                if(Tools.lT.size()!=0)
            Tools.lT.remove(0);
            }
            count++;
            if(count==100000){
                javax.swing.JOptionPane.showMessageDialog(null, "用时间: "+(System.currentTimeMillis()-time1));
                System.exit(0);
            }
        
        }
        }
}
当list不为空的时候,将list中的玩具对象,一个一个取出来,玩完!这个小孩可真够厉害的,呵呵。可以想象为,该类的工作就是不停地向list中取出玩具对象。OK,再来编写方法,如下

主方法
public class MainTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Factory f = new Factory();
        f.start();
        Kid k = new Kid();
        k.start();
        
    }
}

最后是Tools类,他里面有个静态的List对象:
Tools类
public class Tools {
    public static List<Toy>lT = new ArrayList<Toy>(10000);
}

这样,我们运行一下主方法,看看我们这位“天才”玩完100000个玩具,得花销多少的时间。

 

 


好,31毫秒。


    这是我们的第一种解决方法,下面再来看第二种解决方法:
其实我们在Factory类和Kid类中都进行了同步处理,这样一来,浪费了很多时间,到底是不是这样的呢?我们可不可以直接用一个不用处理线程同步的容器来放Toy类对象呢?这样以来是不是就可以节省很多开销了?这个想法是有道理的,但是,事实是不是这样的呢?马上实践!

代码就不具体贴出来了,只是我们在Tools类中用到的是一个如下的对象
Public static LinkedBlockingQueue<Toy> lT= new LinkedBlockingQueue<Toy>(1000);

对,阻塞队列,这样我们就只管往里面取,从里面拿了,不用自己考虑同步问题,Factory类和Kid类中也不同特意去加关键字进行同步了。
那么这种方案的结果是多少呢?同样是100000个玩具,看结果
   

 

 

 


哦哦,变成16毫秒了,着实提高了不少效果呢。看来,在处理同步的时候挤时间,是有发展空间的,呵呵。

等等,有人要发话了,你在这磨叽了半天,还是没有说什么是双缓冲啊,对!有了前面的两种方案,我们再来看看“双缓冲队列”。

所谓双缓冲队列,自然起码要有两个队列吧,呵呵,在这个例子中,我们可以设计两个List来存放工厂生产出来的玩具对象。
下面分析一下:
两个List,一个用来存,一个用来取。有点迷糊?就是有一个listP从工厂那里得到玩具对象,另外一个listT就专门把它得到的玩具对象送去给Kid类处理。当listT变成空的了以后,再将listP中在这段时间内取到的所有玩具对象放到listT中,好,这完了之后,他们两个就又各自干各自的去了:listP再去取,listT再去送。这样是不是就减少了很多次的线程同步呢?至少,在它们交换之前,listP是完全被工厂类线程占有,listT是完全被Kid类线程占有的,不用处理同步。只有在listT放完了,没得给了,再去跟ListP换过来,这个时候就要处理同步了。

跟实际联系一下,有两个搬运工A,B,A在工厂,专门从工厂取玩具;B在小孩子身边,专门送玩具给小孩玩。当B身上没有玩具了,自然要回A那里,把A身上的玩具全部拿过来,再来送给小孩玩。在A还有玩具的时候,A和B是在各自的线程里被处理的,即A在拿,B在给。不用担心同步问题。
这样以来,处理同步问题的次数是不是大大减少了呢?没错,就是这样的。那么怎么跟代码结合呢?
我们要设计一个监视线程,监视listP是不是空了,要是空了,把它同步起来,把listT也同步起来,让他们交换。完了就各自干各自的了。
我们来看看这个监视类:

public class DoubleBufferList {

    private List<Object> lP;
    private List<Object> lT;
    private int gap;

    /**
     * 构造方法
     * 
     * @param lP
     *            用来存放对象的队列
     * @param lT
     *            用来取对象的队列
     * @param gap
     *            交换的间隔
     */
    public DoubleBufferList(List lP, List lT, int gap) {
        this.lP = lP;
        this.lT = lT;
        this.gap = gap;
    }

    public void check() {
        Runnable runner = new Runnable() {
            public void run() {
                while (true) {
                    if (lT.size() == 0) {
                        synchronized (lT) {
                            synchronized (lP) {
                                lT.addAll(lP);
                            }
                            lP.clear();
                        }
                    }
                    try {
                        Thread.sleep(gap);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runner);
        thread.start();
    }

}


这个类中的线程方法就是用来交换的,目的就是为了减少处理同步的次数。这种方案中,Facotry类和Kid类跟前面单个List的情况差不多,就不再给出了。只是有一点要注意,Facory类中将玩具对象是放到了lP中,而Kid类中,也只是从lT中去取对象。看看Tools类中,多了一个变量:
Tools类,声明两个队列
    public static List<Toy>lT = new ArrayList<Toy>(10000);
    public static List<Toy>lP = new ArrayList<Toy>(10000);

同样是让我们的“天才”玩完100000个玩具,来看看运行需要的时间:

 

 

 


 
哈哈,似乎跟我们上面的第二种方案,单阻塞队列,没有太大的差异。怎么解释呢?
不用着急,来,我将额定的玩具量后多加个“0”,让他玩完1000000个!改一下单阻塞队列方案的输出结果,给他们一个标记。再来看看结果:

  
效果出来了吧,我们再加大量,让他们同时处理10000000个玩具对象: 

 

 

 

充分说明,使用双缓冲队列,比单缓冲阻塞队列的效果要好,更别说单缓冲队列了。

总结:
从上面的分析,我们可以得知,在处理线程同步的时候,是要花费我们的时间的,虽然在有些时候,这样的花费是我们可以接受的,但是在很多情况下,如果我们能注意到这样的浪费,并且及时地完善我们的程序,这样可以更大限度地提高我们程序的运行效率。尤其是在大的程序里面,这样的效果体现得更明显。而往往越大的系统,对性能的要求也就越高。

希望这次对“双缓冲队列”的分析和设计,能对各位以后的开发带来一些帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值