一,遇到的问题。
在工作项目中,数据处理完成后合并数据放入一个LinkedTransferQueue中,会有一些长线程来不断地从队列中拿写到数据库。
由此产生以下问题:
1)因为LinkedTransferQueue是无界的,同时使用了transfer + take解决了一部分问题,但是程内部运行的数据处理线程数量多的时候,只有没有限制,会产生Full GC。
2)LinkedTransferQueue是无锁的,但是内部数据结构使用LinkedList,不断添加,删除元素会导致重复gc
基于以上问题想到两个方式:
1)实现一个LinkedTransferQueue的子类,在外层协助控制。(想法来自Motan,Tomcat的线程池设计)
附一份Motan线程池中对LinkedTransferQueue的改造。
Motan线程池中queue参考折叠原码
class ExecutorQueue extends LinkedTransferQueue<Runnable> {
private static final long serialVersionUID = -265236426751004839L;
StandardThreadExecutor threadPoolExecutor;
public ExecutorQueue() {
super ();
}
public void setStandardThreadExecutor(StandardThreadExecutor threadPoolExecutor) {
this .threadPoolExecutor = threadPoolExecutor;
}
// 注:代码来源于 tomcat
public boolean force(Runnable o) {
if (threadPoolExecutor.isShutdown()) {
throw new RejectedExecutionException( "Executor not running, can't force a command into the queue" );
}
// forces the item onto the queue, to be used if the task is rejected
return super .offer(o);
}
// 注:tomcat的代码进行一些小变更
public boolean offer(Runnable o) {
int poolSize = threadPoolExecutor.getPoolSize();
// we are maxed out on threads, simply queue the object
if (poolSize == threadPoolExecutor.getMaximumPoolSize()) {
return super .offer(o);
}
// we have idle threads, just add it to the queue
// note that we don't use getActiveCount(), see BZ 49730
if (threadPoolExecutor.getSubmittedTasksCount() <= poolSize) {
return super .offer(o);
}
// if we have less threads than maximum force creation of a new
// thread
if (poolSize < threadPoolExecutor.getMaximumPoolSize()) {
return false ;
}
// if we reached here, we need to add it to the queue
return super .offer(o);
}
}
|
2)干扰器。
1,Disruptor采用无锁设计。速度加快
每个生产者或消费者线程,会先申请可以操作的元素在分段中的位置,申请到之后,直接在该位置写入或读取数据。
2,Disruptor采用环状结构。避免gc,缓存机制使速度改变。通过设置RingBuffer来控制缓存大小,避免了无界的一系列问题。
避免,垃圾回收,采用双重而非链表。同时,对处理器的缓存机制更加友好。
3,元素位置定位。
索引长度为2,则长度为2 ^ n,通过位运算,加快定位的速度。下标采用增量的形式。不用担心索引重叠的问题。。
二,Disruptor使用。
1,示范
1)定义要传递的消息类。
消息类折叠原码
public class QueueMessage implements Serializable {
public QueueMessage(){}
private List<String> messageList;
public void setMessageList(List<String> messageList){
this .messageList = messageList;
}
public List<String> getMessageList(){
return this .messageList;
}
}
|
2)生产类
生产类折叠原码
public class DisrutorProducerDemo {
private final RingBuffer<QueueMessage> ringBuffer;
public DisrutorProducerDemo(RingBuffer<QueueMessage> ringBuffer) {
this .ringBuffer = ringBuffer;
}
public void onData(String orderId) {
long sequence = ringBuffer.next();
try {
QueueMessage message = ringBuffer.get(sequence);
List<String> messageList = new ArrayList<String>();
messageList.add(orderId);
message.setMessageList(messageList);
} finally {
ringBuffer.publish(sequence);
}
}
}
|
3)消费类
消费类折叠原码
public class EventHandlerDemo implements EventHandler<QueueMessage>, WorkHandler<QueueMessage> {
public void onEvent(QueueMessage event, long sequence, boolean endOfBatch) throws Exception {
System.out.println( "data:" + event.getMessageList().get( 0 ) + ", sequence: " + sequence + ", endOfBatch:" + endOfBatch);
}
public void onEvent(QueueMessage event) throws Exception {
System.out.println( "event:" + event.getMessageList().get( 0 ));
}
}
|
4)组合使用:
Disruptor组合使用折叠原码
public class DisrutorMainDemo {
public static void main(String[] args) throws InterruptedException {
Disruptor<QueueMessage> disruptor = new Disruptor<QueueMessage>(
QueueMessage:: new ,
1024 * 1024 ,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new YieldingWaitStrategy()
);
//可以传多个handler,多个handler会重复消费每条数据
disruptor.handleEventsWith( new EventHandlerDemo());
//一条数据只能被一个handler消费
//disruptor.handleEventsWithWorkerPool(new EventHandlerDemo());
disruptor.start();
RingBuffer<QueueMessage> ringBuffer = disruptor.getRingBuffer();
DisrutorProducerDemo eventProducer = new DisrutorProducerDemo(ringBuffer);
for ( int i = 0 ; i < 100 ; i++) {
eventProducer.onData(UUID.randomUUID().toString());
}
}
}
|
2,Disruptor等待策略
名称
|
措施
|
适用场景
|
---|---|---|
阻塞等待策略 | 加锁 | CPU资源紧缺,骨折和延迟并不重要的场景 |
BusySpinWait策略 | 自旋 | 通过不断重试,减少切换线程导致的系统调用,而降低延迟。推荐在线程绑定到固定的CPU的场景下使用 |
分阶段回退等待策略 | 自旋+ yield +自定义策略 | CPU资源紧缺,骨折和延迟并不重要的场景 |
睡眠等待策略 | 自旋+屈服+睡眠 | 性能和CPU资源之间有很好的折中。连续不均匀 |
超时阻止等待策略 | 加锁,有超时限制 | CPU资源紧缺,骨折和延迟并不重要的场景 |
屈服等待策略 | 自旋+产量+自旋 | 性能和CPU资源之间有很好的折中。连续比较均匀 |
三,性能对比测试:
1,使用Disruptor前后性能对比结果
|
数据量
|
处理起始时间
|
处理结束时间
|
耗时
|
---|---|---|---|---|
优化前 | 1000W | 2020-08-25 18:57:46 | 2020-08-25 19:02:52 | 5.1分钟 |
优化后 | 1000W | 2020-08-26 11:54:10 | 2020-08-26 11:56:58 | 2.8115分钟 |