一次错误使用 synchronized 同步锁导致的问题
前几天公司新人小A跑来问我,说他的一个 ArrayList 无法进行 add 操作了,让我帮他看看。原来他使用一个 ArrayList 作为文件下载进度的存放队列,再使用另一个线程不停地取队列的对象写到数据库,是一个典型的生产者-消费者模型。简化的实现代码是这样的:
private List<Progress> progressList = new ArrayList<>();
//生产线程
synchronized(progressList){
progressList.add(new Progress());
}
//消费线程
while(true){
synchronized(progressList){
Progress progress = progressList.get(0);
if(progress != null){
progressList.remove(0);
//work with data
}
}
}
问题显而易见,为了保证列表的线程安全,代码使用了 synchronized 关键字保证生产和消费的同步,问题出在把同步代码块外面加了死循环,导致这个锁一直被消费线程持有,生产线程无法得到锁自然无法进入操作列表的代码块,导致入队一直失败。
其实使用 List 和 synchronized 实现消费队列是非常低效的做法,Java 并发包提供了 ArrayBlockingQueue、LinkedBlockingQueue 和 LinkedBlockingDequeue 三个线程安全的队列非常适合用来实现生产者-消费者模型的,它们都实现了 BlockingQueue 接口,主要提供了 put 和 offer 两个入队方法以及 take 和 poll 两个出队方法,其中 offer 和 poll 可以设置阻塞超时时间,而 put 和 take 则会一直阻塞直到队列可以进行入队或出队。
由于 LinkedBlockingQueue 使用链表实现,容量没有限制,适合当前业务需要,可以用 LinkedBlockingQueue 将以上代码进行重构,简化代码如下:
private BlockingQueue<Progress> progressQueue = new LinkedBlockingQueue<>();
//生产线程
progressQueue.put(new Progress());
//消费线程
while(true){
Progress progress = progressQueue.take();
//work with data
}
这样一来既省去了自己进行线程同步的工作量,也使代码更加精简直观。在实际开发中应该尽可能利用 Java 并发包提供的容器,而不是自己去实现线程同步。