一、线程池创建四种方式
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
二、有返回值的多线程
ExecutorService接口继承自Executor,Executor中的execute方法无返回值,ExecutorService接口中的方法有返回值。
三、计数器使用
CountDownLatch也是juc包中的一个类,类似倒计时计数器,创建对象时通过构造方法设置初始值,调用CountDownLatch对象的await()方法则处于等待状态,调用countDown()方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
有了计数器就可以暂时将主线程阻塞,等异步的多线程全部执行完毕并返回结果后,再继续执行主线程。
四、线程安全问题
有了上面线程池跟计数器的基础,现在可以动手写一个多线程处理任务并合并数据的demo了。
大致思路就是:创建一个定长的线程池,长度为10,计数器初始值也设置为10。每执行一次,将计数器减一,并且将执行结果添加到list集合中,最终多线程全部执行完毕后,计数器停止等待 主线程继续往下执行,返回list。
public static List<String> getExecutorService() throws InterruptedException{
System.out.println("开始执行多线程...");
long startTime = System.currentTimeMillis();
List<String> list = new ArrayList<>();//存放返回结果
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
Runnable runnable = new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
list.add(UUID.randomUUID().toString());
System.out.println("当前线程name : "+Thread.currentThread().getName());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executorService.execute(runnable);
}
countDownLatch.await();
System.out.println("submit总共cost 时间:" + (System.currentTimeMillis()-startTime)/1000 + "秒");
executorService.shutdown();
return list;
}
执行结果如下:
十个线程全部工作,但是返回值中却有null值,跟想要的结果有点出入,为啥呢?
原因在于:ArrayList是非线程安全的。ArrayList的add方法中有size++,不是一个原子操作,所以线程不安全。
五、CopyOnWriteArrayList的用法
part4中提到的问题 解决方案很简单,将ArrayList换成CopyOnWriteArrayList即可。
public static List<String> getExecutorService() throws InterruptedException{
System.out.println("开始执行多线程...");
long startTime = System.currentTimeMillis();
List<String> list = new CopyOnWriteArrayList<>();//存放返回结果
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
Runnable runnable = new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
list.add(UUID.randomUUID().toString());
System.out.println("当前线程name : "+Thread.currentThread().getName());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executorService.execute(runnable);
// 或者用CompletableFuture.runAsync()执行多线程任务
CompletableFuture.runAsync(
()->{
try {
Thread.sleep(3000);
list.add(UUID.randomUUID().toString());
System.out.println("当前线程name : "+Thread.currentThread().getName());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, executorService);
}
countDownLatch.await();
System.out.println("submit总共cost 时间:" + (System.currentTimeMillis()-startTime)/1000 + "秒");
executorService.shutdown();
return list;
}
CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器"。
优点:读操作时性能很高,因为不需要任何同步措施,适用于读多写少的并发场景。
缺点:①.每次写操作都要copy原容器,频繁的GC,内存压力大。②.由于读写分离的策略,读到的数据很可能是旧数据。