每天有一次定时任务,需要向300w用户推送支付宝模板消息,单台服务器初次尝试30w个消息推送时,耗时4.6个小时,使用的是并行流推送的方式。
通过监控观察到,推送模板消息时,服务器CPU的利用率只在 10% 左右,并没有高效利用起CPU,并行流的优点是将CPU资源调动起来,实现并行处理,一般情况CPU资源应该是被占用得挺多的,假设服务器有四个核,默认并行流会将四个核都同时调用起来,实现真正的并行处理,而不像线程是伪实时,可以将四个核心下的并行流处理单成四个队列,30w任务平均分配在四个队列中,只有前面的任务执行完了,下一个任务才会继续,此时就会面临一个问题了,推送模板消息是发送http请求,涉及到了网络调用,此时每个任务执行过程中是有一个I/O阻塞时间的,下一个任务只能等待前一个完成才会执行,这段I/O阻塞便停滞了CPU资源。
可以通过提高并行流的核心数或者换成线程处理的方式来验证猜测。
@Service
public class ConsumMsg {
@Autowired
StreamOpt streamOpt;
/**
* 初始化任务队列
* @param count
* @return
*/
public List<Integer> initMsg(int count) {
List<Integer> list = Lists.newArrayList();
for (int i = 0; i < count; i++) {
list.add(i);
}
return list;
}
/**
* 并行流处理
* @param count
*/
public void parallel(int count) {
List<Integer> list = initMsg(count);
int successSize = list.parallelStream().mapToInt(streamOpt::sendMsg).sum();
System.out.println("并行流成功个数:" + successSize);
}
/**
* 线程池处理
* @param count
* @param maxThreadCount 最大线程数
* @param initThreadCount 初始线程数
* @param latch
* @throws ExecutionException
* @throws InterruptedException
*/
public void normalThread(int count, int maxThreadCount, int initThreadCount, CountDownLatch latch) throws ExecutionException, InterruptedException {
List<Integer> list = initMsg(count);
// 等待的其他线程则存储到队列里面
ThreadPoolExecutor executor = new ThreadPoolExecutor(initThreadCount, maxThreadCount, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(60000));
// int sum = 0;
for (int i = 0; i < list.size(); i++) {
Future result = executor.submit(new SendMsgThread(i, latch));
// 结果转换耗时很长,需要取消
// sum += (Integer) result.get();
}
// System.out.println("线程池成功个数:" + sum);
}
/**
* 线程
*/
class SendMsgThread implements Callable {
int msg;
CountDownLatch latch;
public SendMsgThread(int msg, CountDownLatch latch) {
this.msg = msg;
this.latch = latch;
}
@Override
public Integer call() throws Exception {
int result = streamOpt.sendMsg(msg);
latch.countDown();
return result;
}
}
}
@Service
public class StreamOpt {
public int sendMsg(int msg) {
// todo 网络调用
return 1;
}
}
public class ConsumTest extends BaseTest {
@Autowired
ConsumMsg consumMsg;
@Test
public void test() throws InterruptedException, ExecutionException {
int count = 100;
CountDownLatch latch = new CountDownLatch(count);
Stopwatch stopwatch = Stopwatch.createStarted();
consumMsg.parallel(count);
System.out.println("并行流耗时:" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");
System.out.println("==============");
// 线程池反而多了几种状态,比如countDownLatch计数,实验结果比并行流还快
Stopwatch watch = Stopwatch.createStarted();
consumMsg.normalThread(count, 120, 100, latch);
latch.await();
System.out.println("线程池耗时:" + watch.elapsed(TimeUnit.MILLISECONDS) + "ms");
}
}
public class ConsumTest extends BaseTest {
@Autowired
ConsumMsg consumMsg;
@Test
public void test() throws InterruptedException, ExecutionException {
int count = 100;
CountDownLatch latch = new CountDownLatch(count);
// 指定并行流线程数
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");
Stopwatch stopwatch = Stopwatch.createStarted();
consumMsg.parallel(count);
System.out.println("并行流耗时:" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");
System.out.println("==============");
// 线程池反而多了几种状态,比如countDownLatch计数,实验结果比并行流还快
Stopwatch watch = Stopwatch.createStarted();
consumMsg.normalThread(count, 120, 100, latch);
latch.await();
System.out.println("线程池耗时:" + watch.elapsed(TimeUnit.MILLISECONDS) + "ms");
}
}
当并行流线程数保持默认时,线程方式消费任务效率高很多,但设置了并行流线程数后,并行流效率也提升了,只不过相比线程方式还是慢了1/3左右。
磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集型的操作,就比如上诉描述的消息推送,涉及到了大量I/O,使用并行流反而慢了很多。
计算类型就属于CPU密集型了,这种操作并行流就能提高运行效率。
http://www.importnew.com/14742.html
https://www.itranslater.com/qa/details/2110153885791814656
《java 8 in action》第七章 并行数据处理与性能