公司之前有个任务,要求删除一张数据库表里面2018/2/1之前的数据。这张表里面存放的是车辆定位数据,一辆车每天能产生4000+条定位数据,所以整个表蛮大的,有65亿+条数据。而且还有要求:根据每个地区要统计出来这个地区删除了多少条数据。其中2月1号之前的有10亿多条。当然这是删除完之后才统计出来的。
一开始是这样做的:查询某个地区,时间在2018/2/1之前的数据,用Mongo游标hasNext()遍历,当遍历出的结果有500条时,用线程池删除。大概代码是这样的:
while (mongoCursor.hasNext()) {
i++;
list.add(mongoCursor.next());
//批量删除500条数据
if (i >= 500) {
pool.execute(new DeleteRunnable(list));
i=0;
list = new ArrayList<>();
}
}
程序运行时发现:mongoCursor.hasNext()方法时常"卡死",导致后面的语句无法执行,但又不抛出异常,导致一天删不了多少数据。原因到现在未知...
但是后面改成了这个方法也有问题:主线程每次查找1w条数据,分给线程池去做删除操作,线程每次删除500条数据。这种方法的问题是:主线程第1次查询出A数据后,线程池还没来得及删除,主线程第2次查询又把A数据查询出来了,交给线程池做删除操作。程序运行后发现:1w条数据,大约有一半数据已经不存在,导致删除失败...
查询1w条数据大概需要70s左右(好慢,而且还是根据索引查询...),删除500条数据大约需要10s左右(根据主键_id删除),所以我改成了每次查询1w条数据后,休眠10s...情况好多了...
前面讲了这么多还是为了引出CountDownLatch和CyclicBarrier,CountDownLatch是后面公司的同事告诉我的。他的提议是,用sleep休眠也只能粗略地估算删除的时间,主线程查询数据后,可以用CountDownLatch的await方法阻塞主线程,等待线程池中的线程都执行了删除方法后,再重新查询数据库。大概代码:
//findDatas为查找出来的1w条数据
int count = findDatas.size() % 500 == 0 ? (findDatas.size() / 500) : (findDatas.size() / 500 + 1);
CountDownLatch countDownLatch = new CountDownLatch(count);
List<String> deleteIDs = new ArrayList<>();
for (int i = 0; i < findDatas.size(); i++) {
deleteIDs.add(findDatas.get(i));
//批量删除500条数据
if (deleteIDs.size() >= 500 || i == findDatas.size() - 1) {
pool.execute(new DeleteRunnable(deleteIDs, countDownLatch));
deleteIDs = new ArrayList<>();
}
}
System.out.println("任务分配完成,主线程等待任务完成");
try {
//主线程阻塞等待
countDownLatch.await();
continue;
} catch (InterruptedException e) {
e.printStackTrace();
}
后来去网上找了一下CountDownLatch这个类,发现很多大佬的博客都是把它和CyclicBarrier放在一起讲,那我也就拜读了几篇大佬的文章,自己做下总结。
CountDownLatch
public CountDownLatch(int count):构造方法,声明有count把锁。
public void countDown():锁的个数减一。
public void await():此方法会阻塞当前线程,直到锁的个数减为0。
public boolean await(long timeout, TimeUnit unit):此方法会阻塞当前线程,直到锁的个数减为0 或者 超时。(对比Future类,此方法超时不会抛出异常)
public long getCount():当前还剩多少把锁。(返回值 >=0,不会出现为负数的情况)
CyclicBarrier
public CyclicBarrier(int parties):构造方法,声明CyclicBarrier屏障要拦截的数量parties。
public CyclicBarrier(int parties, Runnable barrierAction):构造方法,声明屏障要拦截的数量parties,并且在拦截完parties数量后,执行barrierAction方法。
public int getParties():返回障要拦截的数量,也就是构造方法中的parties的值。
public int await():此方法会阻塞当前线程,直到到达屏障的数量为parties。
public int await(long timeout, TimeUnit unit):此方法会阻塞当前线程,直到到达屏障的数量为parties 或者 超时。(超时会抛出TimeoutException,导致其他线程调用await方法会抛出BrokenBarrierException)
public boolean isBroken():当前屏障是否处于broken状态。
public int getNumberWaiting():返回已经到达屏障被阻塞的数量。
public void reset():重置屏障为初始状态,如果有线程正在等待障碍,会抛出BrokenBarrierException。处于broken状态的屏障重置会很复杂,所以如果当前屏障处于broken状态,建议创建一个新的CyclicBarrier对象会更好。
CyclicBarrier和CountDownLatch对比
当然是相比于CountDownLatch只能用一次,CyclicBarrier能复用啦....
CyclicBarrierTest类中,进行了两次比赛,用的都是同一个CyclicBarrier对象,代码:
public class MyRunnable1 implements Runnable {
CyclicBarrier cyclicBarrier;
public MyRunnable1(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + String.format(":我前面有%s人已经准备好了。", cyclicBarrier.getNumberWaiting()));
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public class CyclicBarrierTest {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("所有人到齐,比赛开始.....");
System.out.println("恭喜rng...");
}
});
System.out.println(String.format("五排需要几个人?\t答:%s人", cyclicBarrier.getParties()));
for (int i = 0; i < 5; i++) {
TimeUnit.SECONDS.sleep(1);
new Thread(new MyRunnable1(cyclicBarrier)).start();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("finished...");
System.out.println("\n=============第二次比赛开始...===================\n");
for (int i = 0; i < 5; i++) {
TimeUnit.SECONDS.sleep(1);
new Thread(new MyRunnable1(cyclicBarrier)).start();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("finished...");
}
}
结果: