ConcurrentLinkedQueue事故
伪代码:
private static final long MAX_QUEUE_SIZE = 1_0000_0000;
private ConcurrentLinkedDeque<T> linkedDeque = new ConcurrentLinkedDeque<T>();
public void testQueue(T event){
if (linkedDeque.size() < MAX_QUEUE_SIZE){
linkedDeque.add(event);
}
}
乍一看解决的不错,考虑到了任务超出的抛弃,而且支持高并发。
但是有一个潜在威胁就是当队列的上限足够大时,频繁的使用size,会导致CPU占用瞬间飙升甚至100%。
原因是因为Concurrent系列的集合队列等size方法并不是插入时自增的标记位,而是通过遍历拿到最终结果。
那么时间复杂度就不是O(1)而是O(n)。
ArrayBlockingQueue的OOM
比如在异步日志的打印中,日志事件的载体就是ArrayBlockingQueue,一个有界队列。
如果设置的队列太大,就会产生内存溢出。所以需要限制些策略,比如设置临界值,队列容量达到临界值时,根据配置的抛弃策略,选择挑选符合条件的事件入队。比如异步日志中,达到临界时,选择丢弃小于INFO_INT级别的的任务。put()方法是阻塞的,所以选择不抛弃任务策略时,使用put,会阻塞起来等待队列空闲。抛弃策略则是使用offer()方法,如果满,则直接返回。
ConcurrentHashMap注意事项
比如在并发情况下,如果需要同时收集各自服务上的信息,如果使用ConcurrentHashMap,虽然保证了并发下的线程安全,但是却忽略了map.put()最原始的,Key相同则value覆盖的机制。所以当有此类事件的时候,选用map.putIfAbsent()方法。putIfAbsent会返回当前重复key的value,就可以将这部分内容重新读写。
SimpleDateFormat 线程不安全
多线程模式下,SimpleDateFormat是不安全的。因为内部实现的方式是将日期数据设置cal对象。当其他线程走到这里的时候刚好删掉了这个对象,则会造成异常。
解决办法:
1,每次都new一个新的SimpleDateFormat。但是有些浪费内存。
2,加锁,synchronized。但是竞争锁会消耗资源。
3,使用ThreadLocal。只实现一个SimpleDateFormat实例,节省资源。但是要记得使用完毕清理内存。否则会造成内存泄露。也就是ThreadLocal的问题。threadlocal.remove();
Timer的实现原理
定时任务Timer的底层是使用了一个TaskQueue。平衡二叉树堆实现的优先级队列。这里有一个缺陷就是一旦产生Interrupted Exception以外的异常,将会导致整个队列内其他的任务全部被清除。所以需要额外注意,队列任务内各种异常的捕获。日常开发中,定时器优先考虑ScheduledThreadPoolExecutor。