多线程在实际代码开发中使用频率非常高,熟练的掌握多线程对于提高工作效率以及代码的水平具有很大的帮助。因此本文对于常见的多线程中的一些问题进行理论知识以及实际代码的演示。
创建一个线程有两种方式,一种是继承Thread类,另外一种是实现Runnable接口。这两者的区别是:继承Thread类的本质是多个线程分别完成自己的任务,实现Runnable接口的本质是多个线程共同完成一个任务。这两种方式有一个共同点是需要创建多个线程,参考“数据库连接池”的概念,创建数据库的连接需要耗费很多时间。同理,创建一个线程和销毁一个线程也是比较费时的操作,因此,“线程池”的概念应运而生。下面通过代码来演示,代码一是采用直接创建多个线程的方式,代码二是通过线程池创建多个线程,同样进行数千次的累加,对耗时进行对比。
import java.util.concurrent.atomic.AtomicInteger; public class Demo1 { private static volatile AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) { long l = System.currentTimeMillis(); for (int i=0; i<1000; i++){ Thread thread = new Thread(new Runnable() { @Override public void run() { atomicInteger.incrementAndGet(); } }); thread.start(); try { thread.join();//必须让线程阻塞等待,否则相当于这个线程没有执行完毕就切换到其他的线程,时间不准确 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(System.currentTimeMillis()-l); System.out.println(atomicInteger); } }
代码一的运行耗时是 453ms。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Demo2 { private static volatile AtomicInteger atomicInteger = new AtomicInteger(0); private static ExecutorService executorService = Executors.newFixedThreadPool(10); public static void main(String[] args) { long l = System.currentTimeMillis(); for (int i=0; i<1000; i++){ executorService.execute(new Runnable() { @Override public void run() { atomicInteger.incrementAndGet(); } }); } executorService.shutdown(); try { executorService.awaitTermination(1,TimeUnit.HOURS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis()-l); System.out.println(atomicInteger); } }
代码二的运行耗时是 22ms。通过对耗时进行对比,我们可以发现,使用线程池可以大大减少运行的时间,因为不用每次都去创建线程和销毁线程,验证之前的理论认知。
关于ExecutorService
ExecutorService的生命周期有 运行、关闭、终止三种状态,其在创建后处于运行状态,调用 shutDown 方法将其平滑关闭,调用 shutDownNow 方法将其强制进行关闭,两者的区别是 shutDown 方法后不再接受新的任务,但是会将当前的任务执行完毕才会关闭;shutDownNow 方法后不管队列中有没有等待执行的任务,都会强制的终止。在实际项目开发中,我们经常使用 shutDown 方法来终止。此外,为防止等待时间过长,通常在调用 shutDown 方法后,立即调用 awaitTermination 方法进行阻塞等待,从而产生同步的效果。
关于Executors
看Executors的源码,我们可以发现其内部有很多静态方法,可以创建多种线程池。因此在实际代码开发中,我们经常使用Executors的静态工厂方法来创建线程池,常见的有四种线程池。
- newFixedThreadPool(采用LinkedBlockingQueue队列--基于链表的阻塞队列)定长线程池,顾名思义,最多创建 数量为参数的线程,当达到最大数量后,线程池的规模不再发生变化。
- newCachedThreadPool(使用SynchronousQueue同步队列)可缓存线程池,灵活创建,灵活回收。
- newScheduledThreadPool(使用DelayedWorkQueue延迟队列)支持定时任务和周期性任务,类似于 Quartz 和 Timer。
- newSingleThreadExecutor(采用LinkedBlockingQueue队列--基于链表的阻塞队列)单线程化的线程池,只会创建一个线程执行任务,可以按照指定顺序执行任务,比如 FIFO、优先级等。