前言
在某一模块中,需要将网络接收到的数据存入Oracle中。这是一个典型的生产者消费者场景,可以使用消息队列隔离生产者和消费者。由于接收的数据频度很高,而Oracle的插入速度较慢,为不影响接收端吞吐量,选择了双缓冲队列作为消息队列。双缓冲队列的原理是一般情况下生产者使用写队列,消费者使用读队列,两个线程不需要做同步保护。当读队列消费完的时候,将读写队列交换,生成者使用空的读队列,消费者使用写队列。双缓冲队列只需在这时对读写队列进行同步保护即可,可大幅提高性能。
在使用Java开始编码时发现,Java的多线程同步和C++的差距有点大,怎么会没有条件变量、信号量、互斥锁?百度了几个实例后基本了解了Java的同步策略,比C/C++的要简单不少。不管是条件变量还是互斥锁,synchronized这一个关键字全都搞定。Java每个对象都自带锁,synchronized(obj) { // codeblock } 即可在此代码段保护obj对象;使用obj.wait()、obj.notify()以及obj.nofifyAll()又可以起到条件变量的作用,实在是太方便了。
实现
代码主要由一个双缓冲队列类、一个生产者类和一个消费者类组成。
需要说明的是,在我的这种实现里,只对写队列做了同步保护,这是因为在我的应用里读写队列交换由消费者控制,这样一来,读队列的使用以及交换全在消费者线程,读队列也就没必要同步了。如果改为定时交换的话,就必须对读队列也加保护了。
双缓冲队列类
该类以单例模式实现,queue为单例对象。其中,Emp是我们在队列中要缓存的对象类,如果类型多的话可以将双缓冲队列类改造为模板类。成员变量包含两个列表,一个用来读,一个用来写。几个接口,push用来添加新消息,getWriteListSize获取写队列当前大小,getReadList获取读列表,swap用来交换读写列表。
public class DoubleBufferQueue {
private List<Emp> readList = new ArrayList<Emp>();
private List<Emp> writeList = new ArrayList<Emp>();
private static DoubleBufferQueue queue = new DoubleBufferQueue();
private DoubleBufferQueue() {
}
public static DoubleBufferQueue getInst() {
return queue;
}
public void push(Emp value) {
synchronized (writeList) {
writeList.add(value);
}
}
public int getWriteListSize() {
synchronized (writeList) {
return writeList.size();
}
}
public List<Emp> getReadList() {
return readList;
}
public void swap() {
synchronized(writeList) {
List<Emp> temp = readList;
readList = writeList;
writeList = temp;
writeList.clear();
}
}
}
生产者类
生产者类是一个写线程,负责从服务器读取emp对象的一组实例,放入双缓冲队列的写队列里(代码里与应用相关的部分可以忽略,我临时修改的)。与双缓冲队列相关的调用都在线程函数run里。在调用push函数前,做了个简单的
写入控制:比如当写队列已经缓存了一万个实例时,暂停写入,直到消费者线程用完读队列并交换读写队列。这种方式可能会导致网络吞吐量的降低,但如果能提高消费者效率就可以减少这种降低,比如采用批处理提高Oracle的插入速度。
public class Writer implements Runnable {
private NetSession session = null;
public Writer() {
session = new NetSession();
}
public boolean connect(String server, int port) {
if (!session.connect(server, port)) {
return false;
}
return true;
}
public void run() {
DoubleBufferQueue queue = DoubleBufferQueue.getInst();
while (!session.valid()) {
try {
MsgHead head = session.recvHead();
Byte[] buffer = session.recvBody(head);
if (head.type != EMP)
continue;
Emp emp = new Emp();
emp.deserialize(buffer);
while (queue.getWriteListSize() >= 10000) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
queue.push(emp);
} catch (NetIOException e) {
}
}
}
}
消费者类
消费者类是一个读线程,负责将读队列的元素写入数据库(代码做了改动,写入数据库改为写入文件)。读线程不断检测读队列,当读队列有数据时,遍历读队列读取元素写入数据库。这里 需要注意的是,在读线程里做了交换读写队列的控制,只有当读队列为空且写队列大小超过1000时才进行交换。这样做的好处一是可以避免交换频率过高,二是保证一次获取一定量的实例,可以使用数据库的批处理来提高写入效率。另外,在读队列使用完后,记得要清空读队列。public class Reader implements Runnable {
public void run() {
// TODO Auto-generated method stub
DoubleBufferQueue queue = DoubleBufferQueue.getInst();
try {
while (true) {
List<Emp> readList = queue.getReadList();
while (readList.isEmpty()) {
try {
if (queue.getWriteListSize() > 1000) {
queue.swap();
readList = queue.getReadList();
} else {
Thread.sleep(1);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
int counter = 0;
FileWriter fw = new FileWriter("result.txt", true);
for (Emp value : readList) {
counter++;
fw.write(value.toString());
fw.write("\n");
}
fw.close();
System.out.println("Read: " + counter);
readList.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
应用
应用部分较为简单,创建两个线程,开启运行即可。读写线程中的读写控制考虑的比较简单,有好想法的朋友欢迎交流,谢谢! public static void rwTest() {
Reader reader = new Reader();
Thread t1 = new Thread(reader);
Writer writer = new Writer();
writer.connect("192.168.1.152", 8000);
Thread t2 = new Thread(writer);
t1.start();
t2.start();
}