- 线程池用过哪些?线程池有哪些参数?这几个常用线程池的用法和实际场景?
- 线程池是为了解决大量的请求造成的服务器大量创建和销毁线程所带来的系统资源消耗,并且能够有效控制系统的访问量的一种解决方案;
- 线程池参数:
- corePoolSize:核心线程数;核心线程会一直存活,及时没有任务需要执行;当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程;设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭;创建完线程池之后线程池中的线程数量是0,当任务需要执行任务的时候就创建线程,如果任务数量超过了核心线程数,那么新的任务就会被放入等待队列中,当有线程执行完任务之后,就会把该线程放入线程池,然后从队列中取出任务继续执行;
- maximumPoolSize:线程池最大线程数;当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务;当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常;
- keepAliveTime:线程空闲时间;当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize;如果allowCoreThreadTimeout=true,则会直到线程数量=0;
- TimeUnit:时间单位;
- BlockingQueue:阻塞队列,实现类可以是ArrayBlokingQueue,LinkedBlockingQueue,SynchronousQueue;
- allowCoreThreadTimeout:允许核心线程超时;
- rejectedExecutionHandler:任务拒绝处理器;
- 常用的线程池:
-
CachedThreadPool:
♠ 当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定时长,则该线程会被销毁。
♠ 返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)
执行很多短期异步的程序或者负载较轻的服务器
♠ 执行很多短周期的程序或者负载较轻的服务器 -
FixedThreadPool:
♠ 创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在创建线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入无界的阻塞队列LinkedBlockingQueue;
♠ 返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:LinkedBlockingQueue<Runnable> 无解阻塞队列;
♠ 执行长期的任务,性能好很多; -
SingleThreadExecutor
♠ 创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
♠ FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为: LinkedBlockingQueue<Runnable> 无界阻塞队列;
♠ 一个任务一个任务执行的场景; -
ScheduledThreadPool
♠ 创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务调度,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中(DelayedWorkQueue这是一种按照超时时间排序的队列结构);
♠ 创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:DelayedWorkQueue;
♠ 周期性执行任务的场景;
-
CachedThreadPool:
- 线程池任务执行流程:
- 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
- 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
- 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
- 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
- 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
- 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭;
- 介绍一下你认识的集合框架,并解释Hastable和ConcurrentHashMap相互的关联和区别。
- 所谓集合框架指的就是Collection及其子孙,即List,Set,Queue,Map是一个比较特殊容器,还有两个集合工具Collections和Arrays;如下图:
Java集合框架图 -
List:即有序列表,特点是允许元素重复,记录数据存储的顺序;其常见的实现类:
- 基于数组结构实现:ArrayList,Vector(与ArrayList的区别在于所有的数据操作方法都使用Synchronized修饰);
- 基于链表结构实现:LinkedList;
- Set:即无序列表,特点是元素不允许重复(实现hashCode和equals方法),不记录元素的存储顺序;其常见的实现类:HashSet,TreeSet;
- Map:即存储键值对(Entry)的容器,特点是不允许key重复,不记录数据添加顺序;常见的实现类有:HashMap,TreeMap,LinkedHashMap;
-
Queue:特点是只能从队头(top)取数据,队尾添加数据,先进先出(FIFO),常见的实现类:
- 非阻塞队列:
♠ PriorityQueue:维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(Comparable/Comparator)
♠ ConcurrentLinkedQueue:基于链表结构的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以无需知道队列的大小;ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列;
♠ ArrayDeque:基于数组的双端队列; - 阻塞队列:
♠ ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了 一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于 LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的 完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任 何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而 在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
♠ LinkedBlockingQueue:基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列 中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时 (LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认 一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
♠ LinkedBlockingDeque:基于链表的FIFO双端阻塞队列。
♠ PriorityBlockingQueue:带优先级的无界阻塞队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值;优先级的判断通过构造函数传入的Compator对象来决定,但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度, 否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
♠ SynchronousQueue :并发同步阻塞队列,本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式;一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去 集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中 间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商 品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者 中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别: 如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO(后进先出法 Last In First Out )队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
♠ DelayQueue:DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
- 非阻塞队列:
- LinkedBlockingQueue和ArrayBlockingQueue的说明:
- 一般如果线程池任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。
- 如果线程池任务队列采用ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数(核心线程数)为4,最大为8,所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来从队列中取出任务并执行;这时第二个节流阀—最大线程数(8个)就起作用了。
- Hastable和ConcurrentHashMap
- 二者都可在多线程环境下使用,并能保证线程的安全运行
- Hashtable中所有对数据的操作的方法都是用synchronized关键字进行修饰,也就是说,给整体数据加上了一块大大的锁,当数据量变大的时候,一旦内部出现迭代之类的耗时操作,外部将不能访问数据,只能被阻塞,进而等待锁释放,即性能不稳定;
- ConcurrentHashMap的数据结构在JDK8中有了比较大的变化,而且对于性能来说是比较大的提升;在JDK8之前,ConcurrentHashMap使用的是数组、链表、Segment分段锁(ReentrantLock)实现,而在JDK8的时候的改善是:干掉了Segment分段锁,添加了红黑树的数据结构,默认情况下,当某个链表的长度大于8的时候,这个链表自动转化为一个红黑树,我们都知道,红黑树是一个平衡树,大大的提高了数据的查询插入的效率,但是其算法比较复杂,而且使用CAS+Synchronized的方式保证其数据的安全性。
- 所谓集合框架指的就是Collection及其子孙,即List,Set,Queue,Map是一个比较特殊容器,还有两个集合工具Collections和Arrays;如下图:
- 说说常见的垃圾回收器有哪些?cms回收器有哪几个过程,停顿几次,会不会产生内存碎片?老年代产生内存碎片会有什么问题?详细参考这里
- 垃圾回收器种类
- 新生代的收集器包括:
♦ Serial
♦ PraNew
♦ Parallel Scavenge - 老年代的收集器包括:
♦ SerialOld
♦ ParallelOld
♦ CMS - 回收整个Java堆(新生代和老年代)
♦ G1收集器
- 新生代的收集器包括:
- 垃圾回收器详解
- Serial:串行收集器-复制算法。该收集器是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。而且Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。
- PraNew收集器-复制算法。该收集器是新生代并行收集器,本质上是Serial收集器的多线程版本,包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial 收集器完全一样。
- 垃圾回收器种类
网友评论