基本概念
并发和并行
并发:多个计算任务在同一个CPU内核上进行时间片轮转,从宏观角度来看任务是同时进行的,而实际上多个任务是交替执行的。
并行:多个计算任务在不同的CPU内核上执行,是真正的同时执行。
进程和线程
进程:具有一定独立功能的程序关于一个数据集合的一次运行活动,是一个动态概念。进程是并发执行的程序在执行过程中资源分配的基本单位。
线程:线程是进程的一部分,在一个进程中可以同时运行着多个线程,线程是CPU调度的基本单位。
线程状态转换
(1)新建状态(New):新创建了一个线程对象,尚未启动。
(2)就绪状态(Runnable):调用了start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权。
(3)运行状态(Running):获取了CPU,执行程序代码。
(4)同步阻塞状态(Blocked):运行中的线程请求一个由其他线程占有的排他锁,则当前线程进入同步阻塞状态
(5)无限等待阻塞状态(Waiting):运行中的线程调用wait()方法,进入无限等待阻塞状态。
(6)有限等待阻塞状态(Timed Waiting):运行中的线程调用sleep()或join()方法,或者发出了I/O请求时,则线程进入有限等待阻塞状态。
(7)死亡状态(Terminated):线程执行完毕或者因产生异常而结束。
线程的使用
继承Thread类
1 | public class CustomThread extends Thread { |
实现Runnable接口
1 | public class CustomRunnable implements Runnable { |
实现Callable接口
1 | public class CustomCallable implements Callable { |
线程池
java.util.concurrent包中为我们提供了多钟内置线程池,我们可以通过Executors的静态工厂方法创建线程池,下面一起来看看Java内置的线程池。
FixedThreadPool
1 | ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); |
创建一个定长的线程池,每当提交一个任务就会创建一个线程,直到线程数量达到池的上限。当池中所有线程都处于工作状态,此时提交一个新的任务,则该任务会添加到任务队列中,等待池中的线程可用。
池中的线程会一直存在,如果某一线程由于出现异常而结束,则线程池会补充一个新的线程。
适用于负载较重或可预知工作线程数量的场景。
CachedThreadPool
1 | ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); |
创建一个可缓存的线程池,不对池的长度进行任何限制。
线程默认60s未使用则被回收,当提交新任务时首先考虑复用未被回收的空闲线程,其次考虑新增线程。
适用于负载较轻或短耗时异步任务的场景。
SingleThreadPool
1 | ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); |
创建一个单线程化的线程池,池中仅有一个的工作线程来执行任务,如果该线程由于出现异常而结束,则会创建一个新的线程。
工作线程根据任务队列规定的顺序(FIFO,LIFO,Priority)来执行任务。
适用于需要严格保证任务执行顺序的场景。
ScheduledThreadPool
1 | ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); |
创建一个定长的线程池,可以设定任务执行的延迟时间,周期性执行任务。
适用于线程周期性执行任务的场景。
WorkStealingPool
1 | ExecutorService workStealingPool = Executors.newWorkStealingPool(); |
WorkStealingPool底层使用了ForkJoinPool,默认使用当前可以用CPU数,可将大任务分解为多个小任务,提高CPU的利用率。
适用于耗时较长的计算任务,可将任务进行分解,并行计算。
任务队列
回到Executors的工厂方法中,我们可以看到线程池的构造方法,例如:
1 | public class Executors { |
FixedThreadPool和SingleThreadPool默认使用的是一个无限的LinkedBlockingQueue,当频繁提交任务时,任务数大于工作线程数,这时任务就会进入任务队列中,无限的任务队列会跟随任务的提交而增长,这样就有可能导致资源耗尽,可以使用ArrayBockingQueue、有限LinkedBlockingQueue、PriorityBlockingQueue防止资源耗尽。
对于CachedThreadPool,使用了SynchronousQueue绕开队列,将任务直接交给工作线程,SynchronousQueue不是一个队列,而是一种在线程之间交换信息的机制,由于CachedThreadPool是一个无限的线程池,每提交一个任务都会复用或新建一个线程来执行任务,因此使用SynchronousQueue可以减少任务在队列中维护时放入和取出的性能消耗。
饱和策略
使用有界阻塞任务队列可能会出现任务队列满的情况,当新提交的任务不能进入任务队列时就需要一种饱和策略对任务进行管理,如下:
1 | ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(11)); |
可以通过ThreadPoolExecutor的setRejectedExecutionHandler接口设置饱和策略。Java中提供了四种饱和策略:
(1)AbortPolicy:当新任务不能进入等待队列时,会抛出RejectedExecutionExecption异常,可由调用者捕获,进行相应的处理。
(2)DiscardPolicy:当新任务不能进入等待队列时,会放弃这个任务。
(3)DiscardOldestPolicy:当新任务不能进入等待队列时,会放弃接下来将要执行的任务,如果队列通过优先级排序,则会放弃优先级最高的任务,因此该策略不能同优先级队列同时使用。
(4)CallerRunsPolicy:当新任务不能进入等待队列时,会将一些任务推回给调用者,也就是让调用者线程来执行新任务,这样就使得池线程能够追赶进度,调用者线程在执行新任务时便不会再接受其他的新任务,防止过载时急剧劣化。
ThreadPoolExecutor的默认饱和策略为AbortPolicy。
总结
并发编程是Java中经常使用的关键技术,其中,线程池的使用尤为重要,线程池的使用也有一些需要特别注意的问题,如:
(1)只有当任务彼此相互独立时,使用有限线程池或有限任务队列才是合理的;当任务相互依赖时,可以使用无限的线程池来防止线程饥饿和死锁。
(2)需要特别注意在线程池中使用ThreadLocal管理数据,因为可能存在线程的复用。
参考资料
《Java并发编程实战》