线程池
线程池的原理是通过预先创建一定数量的线程,将它们放入一个队列中,当有任务需要执行时,从队列中取出一个空闲线程来执行任务。当任务执行完毕后,线程会返回队列中等待下一个任务。这样可以避免频繁地创建和销毁线程,提高系统性能。
线程池的主要组成部分包括:
- 核心线程数(corePoolSize):线程池中始终保持活跃的线程数量。
- 最大线程数(maximumPoolSize):线程池允许的最大线程数量。
- 空闲线程存活时间(keepAliveTime):非核心线程闲置的最长时间,超过这个时间的线程将被终止。
- 时间单位(unit):keepAliveTime的时间单位。
- 阻塞队列(workQueue):用于存放等待执行的任务。
- 线程工厂(threadFactory):用于创建新线程的工厂。
- 拒绝策略(handler):当线程池无法处理新任务时采取的策略。
线程池的工作流程如下:
8. 当提交一个新任务时,线程池首先检查当前线程数是否小于核心线程数,如果是,则创建一个新的线程来执行任务;否则,将任务添加到阻塞队列中。
9. 如果阻塞队列已满且当前线程数小于最大线程数,则创建一个新的线程来执行任务;否则,根据拒绝策略处理无法执行的任务。
10. 当一个线程完成任务后,它会返回到阻塞队列中等待新的任务。如果超过了空闲线程存活时间,该线程将被终止。
ThreadPoolExecutor的策略
线程池的状态主要有以下几个:
- RUNNING:线程池处于运行状态,可以接受新任务并处理阻塞队列中的任务。
- SHUTDOWN:线程池不再接受新任务,但会继续处理阻塞队列中的任务,直到队列为空。
- STOP:线程池不再接受新任务,中断所有正在执行的线程,并清空阻塞队列中的任务。
- TIDYING:线程池中的任务已经全部执行完毕,线程池正在整理和清理资源。
- TERMINATED:线程池已经完全终止,所有的资源已经被释放。
线程池的状态转换过程如下:
- 初始状态:创建线程池后,线程池处于RUNNING状态。
- 调用shutdown()方法:线程池进入SHUTDOWN状态,不再接受新任务,但会继续处理阻塞队列中的任务。
- 阻塞队列为空且线程池中没有活跃线程:线程池进入TIDYING状态,开始进行资源的清理工作。
- terminated()方法执行完成:线程池进入TERMINATED状态,所有资源已被释放。
JDK定义的四种线程池
四种常见的线程池分别是:
-
newCachedThreadPool:创建一个可缓存的线程池,它会根据需要创建新线程,但如果线程空闲时间超过60秒,则会被回收。这种类型的线程池适用于执行大量短期异步任务的场景。
-
newFixedThreadPool:创建一个固定大小的线程池,所有线程都是核心线程。当线程池中的线程都在执行任务时,新的任务会等待队列中的任务完成后再执行。这种类型的线程池适用于执行长期运行的任务。
-
newSingleThreadExecutor:创建一个只有一个线程的线程池,这个线程池保证所有任务按照提交顺序依次执行。这种类型的线程池适用于需要顺序执行任务的场景。
-
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。这种类型的线程池适用于需要定时或周期性执行任务的场景。
需要注意的是,《阿里开发手册》建议不要直接使用Executors类中的线程池,而是通过ThreadPoolExecutor的方式创建线程池,这样可以让我们更清楚地了解线程池的运行规则,避免资源耗尽的风险。
阻塞队列
操作方法
阻塞队列的操作方法如下:
插入方法:
- add(e):将元素e添加到阻塞队列中,如果队列已满,则抛出IllegalStateException异常。
- offer(e):将元素e添加到阻塞队列中,如果队列已满,则返回false。
- put(e):将元素e添加到阻塞队列中,如果队列已满,则一直阻塞直到有空间为止。
- offer(e, time, unit):将元素e添加到阻塞队列中,如果在指定的等待时间内队列仍然没有空间,则返回false。
移除方法:
5. remove():移除并返回阻塞队列的头部元素,如果队列为空,则抛出NoSuchElementException异常。
6. poll():移除并返回阻塞队列的头部元素,如果队列为空,则返回null。
7. take():移除并返回阻塞队列的头部元素,如果队列为空,则一直阻塞直到有元素为止。
8. poll(time, unit):移除并返回阻塞队列的头部元素,如果在指定的等待时间内队列仍然为空,则返回null。
检查方法:
9. element():返回阻塞队列的头部元素,但不移除它。如果队列为空,则抛出NoSuchElementException异常。
10. peek():返回阻塞队列的头部元素,但不移除它。如果队列为空,则返回null。
BlockingQueue的实现类
ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue和SynchronousQueue是Java中常用的阻塞队列实现类。它们的主要特点如下:
-
ArrayBlockingQueue:由数组结构组成的有界阻塞队列,内部结构是数组,具有数组的特性。可以初始化队列大小,且一旦初始化不能改变。构造方法中的fair表示控制对象的内部锁是否采用公平锁,默认是非公平锁。
-
LinkedBlockingQueue:由链表结构组成的有界阻塞队列,内部结构是链表,具有链表的特性。默认队列的大小是Integer.MAX_VALUE,也可以指定大小。此队列按照先进先出的原则对元素进行排序。
-
DelayQueue:该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。注入其中的元素必须实现java.util.concurrent.Delayed接口。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
-
PriorityBlockingQueue:基于优先级的无界阻塞队列,内部控制线程同步的锁采用的是公平锁。优先级的判断通过构造函数传入的Comparator对象来决定。
-
SynchronousQueue:这个队列比较特殊,没有任何内部容量,甚至连一个队列的容量都没有。并且每个put必须等待一个take,反之亦然。
锁接口和类
锁的分类
锁的分类主要包括以下几种:
-
可重入锁和非可重入锁:
- 可重入锁:支持一个线程对资源重复加锁,如synchronized关键字和ReentrantLock类。
- 非可重入锁:如果一个线程已经持有锁,再次尝试获取锁时会导致阻塞或异常。
-
公平锁与非公平锁:
- 公平锁:按照请求锁的顺序来获取锁,即先到先得,遵循FIFO原则。
- 非公平锁:不保证请求锁的顺序,可能会导致线程饥饿现象,但效率较高。
-
读写锁和排它锁:
- 排它锁:在同一时刻只允许一个线程进行访问,如synchronized和ReentrantLock。
- 读写锁:允许多个读线程同时访问,但在写线程访问时,所有读线程和其他写线程都会被阻塞。Java中通过ReentrantReadWriteLock类实现。
根据实际需求选择合适的锁类型可以提高程序的性能和并发能力。
并发集合容器
ConcurrentMap接口继承了Map接口,并增加了四个方法:
- putIfAbsent:如果key不存在,则插入元素;如果key相同,则不替换原有的value值。
- remove:如果要删除的key-value与Map中原有的key-value对应不上,则不会删除该元素。
- replace(K, V, V):如果key-oldValue与Map中原有的key-value对应上,才进行替换操作。
- replace(K, V):如果key存在,则直接替换value,不对Map中原有的key-value进行比较。
ConcurrentHashMap类是基于散列表的Map,它提供了一种粒度更细的加锁机制,称为分段锁(Lock Striping),以实现更高的并发性能。在ConcurrentHashMap中,数据被分段,每个段都有自己的锁。这种结构允许多个线程同时访问不同段的数据,从而提高了并发性能。
ConcurrentNavigableMap接口继承了NavigableMap接口,提供了最接近匹配项的导航方法。ConcurrentSkipListMap是ConcurrentNavigableMap的主要实现类,底层使用跳表(SkipList)数据结构,并使用CAS来保证并发安全性。
对于并发Queue,JDK提供了ConcurrentLinkedDeque和ConcurrentLinkedQueue这两个线程安全的队列类,它们使用CAS来实现线程安全。
在并发Set方面,JDK提供了ConcurrentSkipListSet,它是一个线程安全的有序集合,底层使用ConcurrentSkipListMap实现。谷歌的Guava框架也实现了一个线程安全的ConcurrentHashSet。
CopyOnWrite容器是一种线程安全的并发容器,它在写操作时会复制整个容器,而不是在原容器上进行修改。这样可以在多线程环境下实现读写分离,提高读操作的性能。但是,这种容器的缺点是在写操作时需要复制整个容器,可能导致内存压力较大和Full GC频繁。
CopyOnWriteArrayList是CopyOnWrite容器的一个实现,它提供了add、remove等方法。这些方法在执行时会先复制原容器,然后在新副本上进行写操作,最后将原容器引用指向新副本。在这个过程中,需要加锁以保证线程安全。
CopyOnWriteMap是另一个CopyOnWrite容器的实现,它实现了Map接口。它的put和putAll方法也是通过复制原容器并在新副本上进行写操作来实现的。同样,这些方法也需要加锁以保证线程安全。
在实际业务场景中,可以使用CopyOnWriteMap来存储一些不需要实时更新的数据,例如黑名单、配置信息等。由于CopyOnWriteMap在写操作时会复制整个容器,所以适用于读操作远多于写操作的场景。但是,如果需要实时更新数据,建议使用其他线程安全的并发容器,如ConcurrentHashMap。