Java-Thread
Java-Thread
1. Thread简介
1.1 Java中的线程
1.2 Java守护线程
Java 中的线程有两类:User Thread 和 Daemon Thread,引用 Oracle 对 Thread 的介绍:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
- The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.
- All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.
实际上,Linux 本身只有「守护进程」而并没有「守护线程」的概念,Java 借鉴了 Unix 的思想设计了 Thread。但不论是守护线程还是守护进程,其守护的都并不是其他线程 / 进程,而是 创建者 的「生命周期」:
守护进程可参考:Linux-Process。
- Linux 中守护进程的意义是可以在操作系统的后台长期存活而不受到控制终端的影响,直至与操作系统一起结束。
- Java 中守护线程的意义是可以在 JVM 运行时的后台长期存活,直至与 JVM 一起结束。
一个 Java 应用启动后,默认具有一个主线程(User Thread)和一个 GC 线程(Daemon Thread),当 JVM 中所有 User Thread 都结束后,JVM 就会结束守护线程并退出。除此之外 User Thread 和 Daemon Thread 在其他方面并没有区别。
Java 中可以通过 Thread#setDaemon(boolean)
将一个线程设置为守护线程,有几点注意事项:
- 设置守护线程必须在线程 Start 前,否则会抛出
IllegalThreadStateException
异常。 - 守护线程创建的线程将默认也是守护线程。
- 守护线程的意义并不是常驻后台,因为当所有 User Thread 均退出后,JVM 就会结束守护线程并退出,因此不能在守护线程中执行重要操作。
2. 线程池
线程池底层是通过 HashSet 维护的,可以确保不会出现一个线程的多个实例,并且集合无序,可以保持高效率。多余的任务用一个阻塞队列来管理,阻塞队列满了才会激活非核心线程。
2.1 线程池关键参数
corePoolSize
核心线程数:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut
。这里的最小线程数量即是corePoolSize
。maximumPoolSize
最大线程数:一个任务被提交到线程池后,首先会缓存到工作队列中,如果工作队列满了则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize
来指定。keepAliveTime
空闲线程存活时间:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize
,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime
来设定。unit
空闲线程存活时间的单位:keepAliveTime
的计量单位。workQueue
线程管理队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。JDK 中提供了四种工作队列:ArrayBlockingQueue
:基于数组的有界阻塞队列;按 FIFO 排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize
后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize
,则会执行拒绝策略。LinkedBlockingQuene
:基于链表的无界阻塞队列(其实最大容量为Interger.MAX
);按照 FIFO 排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize
后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize
,因此使用该工作队列时,参数maxPoolSize
其实是不起作用的。SynchronousQuene
:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize
,则执行拒绝策略。PriorityBlockingQueue
:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
threadFactory
线程工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为Daemon 线程
等等handler
拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,JDK 中提供了 4 种拒绝策略:CallerRunsPolicy
:该策略下,在调用者线程中直接执行被拒绝任务的run()
方法,除非线程池已经 Shutdown,则直接抛弃任务。AbortPolicy
:该策略下,直接丢弃任务,并抛出RejectedExecutionException
异常。DiscardPolicy
:该策略下,直接丢弃任务,什么都不做。DiscardOldestPolicy
:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
2.2 线程池参数设置
CPU 密集型任务:由于每个线程执行期间都大概率会占用完整的 CPU 时间片,因此通常可以设置为:
1
2
3
4
5
6
7
8
9
10// processorNum 是指 CPU 的逻辑核心数而非物理核心数。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
processorNum, // corePoolSize,需要根据具体情况调整,例如预留 1 ~ 2 个核心给主业务。
corePoolSize + 1, // maximumPoolSize,CPU 密集型通常用 N + 1
10, // keepAliveTime
TimeUnit.MILLISECONDS, // timeUnit
new ArrayBlockingQueue<Runnable>(32), // workQueue,CPU 密集型使用有界队列,防止资源耗尽
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // rejectPolicy,CPU 密集型需要防止资源抢占
);IO 密集型任务:由于每个线程执行期间大概率只会占用很少的 CPU 时间片,因此通常:
1
2
3
4
5
6
7
8
9
10// processorNum 是指 CPU 的逻辑核心数而非物理核心数。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
Math.min(16, (2 * processorNum)), // corePoolSize,IO 密集型可适当增大,16 为经验值
2 * corePoolSize + 1, // maximumPoolSize,IO 密集型可适当增大
10, // keepAliveTime
TimeUnit.MILLISECONDS, // timeUnit
new PriorityBlockingQueue<Runnable>(32), // workQueue,IO 密集型可使用无界队列增加吞吐量
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.DiscardOldestPolicy() // rejectPolicy,IO 密集型通常以最后发生为准
);