`

双缓冲队列尝试

阅读更多

提出背景:
在C/S模式的系统里面,服务器端的主线程,除了要接收消息之外,还要处理消息。这使得主线程的工作量不但很大,而且工作很繁杂。这种情况在软件设计的角度来看,是很不好的:第一,这样让主线程类看起来异常的臃肿和难易阅读,第二,软件设计追求的目标是“尽量让每个类处理的工作都很单一,这样便于以后的调试和进一步对程序的扩展和移植”,这样的设计背离了软件设计中“模块化设计”的原则。
为此,很多程序员会设计一个全局的LinkedBlockingQueue对象来存放服务器主线程所收到的消息,即主线程收到消息之后,不做任何业务逻辑处理,直接put到阻塞队列中去,然后再启动一个处理消息的线程来从阻塞队列中take消息,这样就让服务器端程序大大解耦了,也便于阅读了,当然,最重要的是,效率也提高了不少。
但是在负荷量很大的系统里面,这样做仍然不是最好的办法。上面的解决办法中,仍然存在两个线程竞争一个资源的情况,即仍然存在同步的问题,只是LinkedBlockingQueue在底层进行了处理,在负荷量很大,并且要频繁访问同一个LinkedBlockingQueue对象的情况下,会使得效率较原始情况没有多大的提高。分析一下:主线程每次拿到消息M,都要put到LinkedBlockingQueue对象中,这样在LinkedBlockingQueue底层就要处理一次线程同步问题,当工作量很大很频繁的时候,这样的操作未免就显得有些累赘了,浪费了很多的时间和资源。同样在处理线程,每次take一个消息M,就会处理一次线程同步,同样降低了效率。注意,只是在负荷量很大的情况下(如中国移动CMPP短信网关),这样的弱点才会很明显地暴露出来。

问题分析:
暴露出了问题,就需要我们想办法去解决,通过上面的分析,发现瓶颈在于,我们是对同一个LinkedBlockingQueue进行的操作,才会引起这么多的滞后。那么能不能试着去用两个LinkedBlockingQueue对象进行操作呢?一个专门给主线程put,另外一个专门给处理线程take和处理,当他们处理完了,再来交换?这样的话,就保障了基本上在每个时刻,每个线程都是在处理自己独自占有的资源,没有引发同步问题。这称之为:双缓冲队列。
当然,上面的解决方案是在一种很理想的情况下,即两个队列,在同一时刻被放满和取空,这样他们在程序运行过程中所达到的效率是最高的,因为没有任何消息被延后处理。所有消息都按照原定计划被处理了。现实情况往往没有这般顺利,但是上面的方案也不失为我们提供了一种解决思路,这其中还涉及到很多的处理机制。为了分析方便,我们暂且将两个LinkedBlockingQueue对象分别称之为LP(主线程控制的专门put的队列),LT(处理线程控制的,专门take的队列)。
第一,    LP和LT队列什么时候才进行交换?肯定是在LT将所有的消息处理完了之后才进行交换,如果LT里面的消息都没有处理完就被交换了,那有可能LT里面的消息有些几年甚至永远不会被处理,这样的系统是无法投入使用的。
第二,    如果在LT处理完了,LP中还没有收到任何消息会怎么办?这是一种很极端的情况,但是也必须考虑到,延伸一下,如果LT里面的消息处理完了,而LP里面却只有一两条消息,该怎么办?要继续交换,还是等待一段时间以后再进行交换?在这里,笔者采用的是前者。目的是为了让消息都能及时地被处理。

问题解决:

首先我们来看看不使用双缓冲机制的程序:

package cn.netjava.lbq.copy;
//玩具类
public class Toy {
private String name;

public String getName() {
return name;
}

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

}
 
package cn.netjava.lbq.copy;

//在线程中生产玩具,放入单缓冲队列
public class Factory extends Thread{


public void run(){
while(true){
Toy t = new Toy ();
t.setName("玩具");
try {
Tools.lT.put(t);
System.out.println("生产出一个玩具");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
 
package cn.netjava.lbq.copy;
//从单缓冲队列中取出玩具对象

public class Kid extends Thread {
	long time1 = System.currentTimeMillis();
	int count = 0;
	public void run() {
		while(true){
		try {
		    Tools.lT.take();
			count++;
//计算取100000个所需要的时间
			if(count==100000){
				System.out.println(System.currentTimeMillis()-time1);
				Thread.sleep(100000);
			}
			System.out.println("玩完一个玩具");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		}
		}

}
 
package cn.netjava.lbq.copy;
//存放玩具对象的的队列
import java.util.concurrent.LinkedBlockingQueue;

public class Tools {
	public static LinkedBlockingQueue<Tool>lT = new LinkedBlockingQueue<Tool>(10000);
	
}
 
package cn.netjava.lbq.copy;

public class MainTest {

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

}

 取出100000个玩具的时间为:3609ms

 

 

再来看看使用双缓冲队列的情况,toy类就不再贴出了。

看其他类,中间做了一些改动

package cn.netjava.lbq;

import java.util.concurrent.LinkedBlockingQueue;

public class Tools {
	public static LinkedBlockingQueue<Tool>lT = new LinkedBlockingQueue<Tool>(10000);
	public static LinkedBlockingQueue<Tool>lP = new LinkedBlockingQueue<Tool>(10000);
}
 
package cn.netjava.lbq;


public class Kid extends Thread {
	long time1 = System.currentTimeMillis();
	int count = 0;
	public void run() {
		while(true){
		try {
			 Tools.lT.take();
			count++;
			
			if(count==10000){
			System.out.println(System.currentTimeMillis()-time1);
			Thread.sleep(100000);
			}
			System.out.println("玩完一个玩具");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		}
		}

}
 
package cn.netjava.lbq;

import java.util.concurrent.LinkedBlockingQueue;

public class Factory extends Thread{
	

	public void run(){
		while(true){
		Toy t = new Toy ();
		t.setName("玩具");
		try {
			Tools.lP.put(t);
			System.out.println("生产出一个玩具");
		} catch (InterruptedException e) {
			e.printStackTrace();
	 	}
		 }
	}
}
 
	package cn.netjava.lbq;

import java.util.concurrent.LinkedBlockingQueue;
	/**
	 * 定义一个双缓冲队列
	 * @author Irving
	 * @time  2009-9-16----上午10:25:49
	 *
	 */
	public class DoubleBufferList {
	
		private LinkedBlockingQueue<Object> lP;
		private LinkedBlockingQueue<Object> lT;
		private int gap;
		/**
		 * 构造方法
		 * @param lP 用来存放对象的阻塞队列
		 * @param lT 用来取对象的阻塞队列
		 * @param gap 交换的间隔
		 */
		
		public DoubleBufferList(LinkedBlockingQueue  lP
				,LinkedBlockingQueue  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();
		}
		
	}
 
package cn.netjava.lbq;

public class MainTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Factory f = new Factory();
		f.start();
		Kid k = new Kid();
		k.start();
		new DoubleBufferList(Tools.lP,Tools.lT,1).check();
	}

}

同样100000个玩具对象,使用的时间是3469ms,大约节约了200ms

 

在使用双缓冲队列的时候,要注意调试监测线程的监控时间间隔和阻塞队列的大小,这些都是影响运行速度的关键因素。

 

分享到:
评论
3 楼 大隐于市 2011-09-13  
既然用了同步,就没必要再用LinkedBlockingQueue了吧,用普通的list效率会更高。
2 楼 Irving_wei 2009-09-16  
sahero 写道
本人有两个问题:
1.博主提出的性能问题是不是由于lT.take(),lT.put()都作用于同一个对象造成的?
2.对于
if(lT.size()==0){  
                lT.addAll(lP);  
                lP.clear();  
            }  
当程序已经执行了lT.addAll(lP)还未执行lP.clear(),这时候如果lP.put(t)又放进了一个对象,那么这个对象不就会被清理掉了?


呵呵,第一点,我个人觉得性能问题是由于lT.take(),lT.put()都作用于同一个对象造成的。
      第二点,程序应该这样改下才合适    if(lT.size()==0){
synchronized(lT){
synchronized(lP){
lT.addAll(lP);
}
lP.clear();
}
}
谢谢提醒啊。我现在正在做一个关于CMPP的模拟程序,关于改善线程同步来提高性能方面也在考虑当中,希望有机会可以和你讨论讨论啊。
1 楼 sahero 2009-09-16  
本人有两个问题:
1.博主提出的性能问题是不是由于lT.take(),lT.put()都作用于同一个对象造成的?
2.对于
if(lT.size()==0){  
                lT.addAll(lP);  
                lP.clear();  
            }  
当程序已经执行了lT.addAll(lP)还未执行lP.clear(),这时候如果lP.put(t)又放进了一个对象,那么这个对象不就会被清理掉了?

相关推荐

    一个c++环形队列缓冲区

    ### 环形队列缓冲区的关键知识点 #### 一、环形缓冲区的基本概念与应用 环形缓冲区(Circular Buffer),又称循环队列,是一种高效的数据结构,在嵌入式系统、网络通信、多媒体处理等领域有着广泛的应用。它通过在...

    无锁缓冲队列

    无锁缓冲队列是一种在多线程环境下高效且并发友好的数据结构,它在计算机科学和编程领域,尤其是在并发编程和高性能计算中占有重要地位。无锁缓冲队列(也称为原子队列或lock-free queue)的设计目标是通过避免锁的...

    数据结构中的关于队列的实验

    - **缓冲区管理**:如操作系统中的打印任务队列,网络传输的数据包队列等。 - **任务调度**:如进程调度、线程调度等。 - **广度优先搜索(BFS)**:在图论中,BFS遍历算法通常使用队列来存储待访问的节点。 - *...

    生产者/消费者模式 阻塞队列 LinkedBlockingQueue

    同时,作为阻塞队列,当生产者尝试向满队列添加元素时,或者消费者尝试从空队列中获取元素时,线程会被阻塞,直到队列有可用空间或数据,这大大简化了多线程同步的问题。 在生产者/消费者模式中,生产者通常通过`...

    CircularQueue循环队列实现

    如果队列已满,需要处理溢出情况,如双指针重置或扩展队列容量。 - **出队(dequeue)**:移除队头元素并更新队头指针。如果队列为空,需要抛出异常或返回错误信息。 - **查看队头元素(peek)**:不移除队头元素,...

    用于循环缓冲的C++语言代码

    循环缓冲,又称环形缓冲或双缓冲,是一种高效利用内存的方式,它通过一个固定大小的内存区域来存储数据,并允许数据在两端进行读写操作。这种设计允许连续的数据流在缓冲区中无缝流动,而无需频繁地移动或复制数据,...

    各类链表以及栈,队列

    常见应用包括任务调度、打印队列、缓冲区管理等。 在提供的代码中,`MyStack` 类实现了模板化的栈数据结构。类中定义了以下方法: 1. 构造函数 `MyStack()`:初始化栈,将计数器 `count` 设置为0,表示栈为空。 2....

    重绘标题栏闪烁问题怎么搞??

    1. **双缓冲机制缺失**:在进行窗口重绘时,如果没有启用双缓冲,每次画图操作都会直接显示在屏幕上,导致闪烁现象。双缓冲是一种图形绘制技术,它先在内存中的一个缓冲区完成绘制,然后再一次性将结果拷贝到屏幕,...

    界面闪烁,求解决办法

    2. **双缓冲技术**:界面闪烁的一个常见原因是未使用双缓冲技术。双缓冲可以避免在绘制过程中用户看到半成品图像,从而减少闪烁。检查`testDlg`类的实现,看是否启用了双缓冲。如果未启用,可以在`OnPaint()`函数中...

    易语言文本滚动不闪烁源码.7z

    1. **双缓冲技术**:在易语言中,可以利用双缓冲技术来消除文本闪烁。双缓冲是指在内存中创建一个与显示区域大小相同的缓冲区,先将所有图形绘制到缓冲区,最后一次性将缓冲区内容拷贝到屏幕上,避免了频繁的屏幕...

    所有基础数据结构和算法的纯C语言实现,如各自排序、链表、栈、队列、各种树以及应用、图算法、字符串匹配算法、回溯、并查.zip

    4. **队列**:队列是一种先进先出(FIFO)的数据结构,适用于处理任务调度、缓冲区管理等问题。C语言中可以通过数组或链表实现队列。 5. **树**:树是一种非线性数据结构,包含节点和边,具有层次关系。常见的树有...

    single-producer, single-consumer lock-free queue

    队列由一组固定大小的缓冲区组成,头部和尾部索引由原子变量维护。生产者在尾部插入元素,通过原子操作更新尾指针;消费者在头部删除元素,同样通过原子操作更新头指针。 为了确保正确性,无锁队列需要解决以下关键...

    数据结构C语言

    队列是一种先进先出(FIFO)的数据结构,常用于任务调度和缓冲区管理。在C语言中,顺序队列通常用数组实现,而链队列则使用链表。队列的基本操作有入队(enqueue)、出队(dequeue)、检查队首元素(front)和判断...

    數據結構前四章例子代碼

    4. **队列**:队列是一种先进先出(FIFO)的数据结构,适用于任务调度、缓冲等。队列的基本操作包括入队(enqueue)、出队(dequeue)和查看队首元素。队列的实现方式有数组循环队列和链表队列。这里可能包含代码...

    算法_贪心算法_

    2. 队列:队列是一种先进先出(FIFO)的数据结构,常用于任务调度、缓冲区管理等。在贪心算法中,队列可能用于优先级队列的实现,例如Dijkstra的最短路径算法,通过优先级队列来快速找到下一个处理的顶点。 3. 堆:...

    数据结构表栈链队源码

    队列在任务调度、缓冲区管理等领域广泛使用。链表或数组同样可以用来实现队列。 这些源码提供了学习和实践数据结构的良好素材。通过阅读和理解源码,你可以深入理解这些数据结构的工作原理,并能灵活运用到实际项目...

    数据结构课程设计之迷宫

    为了提高效率,可以考虑使用双缓冲技术,先在内存中完成绘制,再一次性刷新到屏幕上,避免闪烁和卡顿现象。另外,如果迷宫较大,可以优化渲染策略,只更新改变的部分,而非整个迷宫。 在解决迷宫问题时,常见的算法...

    【并发编程】自定义简单线程池.pdf

    - 如果达到核心线程数,尝试将任务放入阻塞队列。 3. **执行任务**:线程从阻塞队列中取出任务执行。 4. **回收线程**:当没有任务时,线程被回收。 ##### 3. 设计思路及实现 - **拒绝策略** (`RejectPolicy`): ...

    数据结构试卷通用版的

    队列则是一种先进先出(FIFO)的数据结构,可以使用deque或queue容器实现,常见应用有任务调度、缓冲区管理等。 三、树与图 树是一种非线性的数据结构,每个元素称为节点,包含数据及指向子节点的引用。二叉树是最...

    《数据结构》的全部代码实现(C语言)

    4. **队列**:队列是一种先进先出(FIFO)的数据结构,常用于任务调度、缓冲区管理等。C语言实现队列通常采用数组或链表,包括顺序队列和循环队列。 5. **树**:树形结构模拟了自然界中的层次关系,如二叉树、二叉...

Global site tag (gtag.js) - Google Analytics