线程池

154 阅读15分钟

线程池

1.线程池的作用

什么是线程池?其实和连接池差不多,连接池是存放连接的,线程池是存放线程的。

我们使用线程的时候首先需要创建一个线程,当线程用完的时候需要销毁这个线程,而且这样频繁的创建/销毁线程必然会影响程序的一个处理效率。那么我们能不能让线程复用,就是说当一个线程用完之后不要立即销毁,而是把他放在一个地方,当下次程序需要用线程的时候,我们就直接把这个线程给程序,从而避免了频繁创建和销毁的一个操作,提高程序的性能。在这种想法下,就出现了线程池,线程池就是存放线程的那个地方。当然线程池的作用还有可以控制并发数量(设置最大线程数),还可以对线程的状态进行简单的管理。

总结一下线程池的原理,作用:

1.线程复用:实现线程复用的原理应该就是要保持线程处于存活状态(就绪,运行或阻塞)

2.控制并发数量:(核心线程和最大线程数控制)

3.管理线程(设置线程的状态)

2.线程池的创建方式有哪些?有什么区别、

常见的四种线程池:

(1)可缓存线程池CachedThreadPool()

这种方式创建的线程的特点:

1.这种线程池内部没有核心线程,线程的数量是没有限制的。

2.在创建任务时,若有空闲的线程则复用空闲的线程,若没有就创建一个新的线程

3.没有工作的线程(闲置状态),在超过60s不做事,就会被销毁

4.适用:执行很多短期异步的小程序或者负载较轻的服务器

(2)FixedThreadPool 定长线程池

这种方式创建的线程的特点:

1.该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。

2.如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。

3.适用:执行长期的任务,性能好很多。

(3)SingleThreadPool 单线程池

1.有且仅有一个工作线程执行任务

2.所有任务按照指定顺序执行,即遵循队列的入队出队规则。

3.适用:一个任务一个任务执行的场景。

(4)ScheduledThreadPool能实现定时、周期性任务的线程池

DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。

1.不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。

2.这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。

3.适用:周期性执行任务的场景(定期的同步数据) 总结:除了new ScheduledThreadPool 的内部实现特殊一点之外,其它线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。

创建线程池的核心参数有哪些?每个代表什么意思?

corePoolSize:核心线程数(最小存活的工作线程数量)

maxPoolSize:最大线程数

keepAliveTime:线程存活时间(在corePoreSize<maxPoolSize情况下有用,线程的空闲时间超过了keepAliveTime就会销毁)

timeUnit:存活时间的时间单位

workQueue:阻塞队列,用来保存等待被执行的任务(①synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;②LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;③ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小)

threadFactory:线程工厂,主要用来创建线程;

handler:表示当拒绝处理任务时的策略(①丢弃任务并抛出RejectedExecutionException异常 ;②丢弃任务,但是不抛出异常; ③丢弃队列最前面的任务,然后重新尝试执行任务 ;④由调用线程处理该任务)

线程池中的最大线程数

一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的个数)

如果是CPU密集型应用,则线程池大小设置为N+1

如果是IO密集型应用,则线程池大小设置为2N+1

如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。 但是,IO优化中,这样的估算公式可能更适合:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

创建线程的个数是还要考虑 内存资源是否足够装下相当的线程

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。

线程池的拒绝策略有哪些?

handler:表示当拒绝处理任务时的策略(

①丢弃任务并抛出RejectedExecutionException异常 ;

②丢弃任务,但是不抛出异常;

③丢弃队列最前面的任务,然后重新尝试执行任务 ; ④由调用线程处理该任务)

JVM的优化

什么是JVM?JVM有哪些组成?每一部分的作用?

1.JVM:java虚拟机,主要屏蔽底层与操作系统之间的差异,提供了统一的运行环境,从而实现java的跨平台原理。

2.JVM的成员:

①.类加载器子系统-->这是用于类加载的一个子系统.

②.运行时数据区 -->运行时数据区分为虚拟机栈,本地方法栈,堆区,方法区和程序计数器.

③.执行引擎-->执行引擎包含即时编译器(JIT)和垃圾回收器(GC),

④.本地方法库

类加载的过程:

包括五个步骤:加载-->验证-->准备-->解析-->初始化

    加载:找到字节码文件,读取到内存中。
    验证:验证:验证此字节码文件是不是真的是一个字节码文件,毕竟后缀名可以随便改,而内在的身份标识是不会变的.
    准备:为类中static修饰的变量分配内存空间并设置其初始值为0或null.
    解析:会把java中的符号引用改为代码引用。比如把类的全限定名改为类加载到内存中的地址。
    初始化:对类中的变量进行赋值。

双亲委派机制指的是什么?

以应用程序类加载器举例,它在需要加载一个类的时候,不会直接去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托启动类加载器去加载.

启动类加载器在自己的搜索范围内没有找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载,同样的,扩展类加载器在自己的搜索范围内找一遍,如果还是没有找到,就委托应用程序类加载器去加载.如果最终还是没找到,那就会直接抛出异常了.

而为什么要这么麻烦的从下到上,再从上到下呢?

这是为了安全着想,保证按照优先级加载.如果用户自己编写一个名为java.lang.Object的类,放到自己的Classpath中,没有这种优先级保证,应用程序类加载器就把这个当做Object加载到了内存中,从而会引发一片混乱.而凭借这种双亲委派机制,先一路向上委托,启动类加载器去找的时候,就把正确的Object加载到了内存中,后面再加载自行编写的Object的时候,是不会加载运行的.

运行时数据区有哪些组成?每一部分有什么作用?

1.方法区 :方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。

从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

去永久代的原因有:

(1)字符串存在永久代中,容易出现性能问题和内存溢出。

(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

2.堆内存:堆内存主要用于存放对象和数组,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。

3. 虚拟机栈 :虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。

4.本地方法栈 :本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法(Native Method),其他基本上一致,

5.程序计数器:主要用途还是用来确定指令的执行顺序,比如循环,分支,跳转,异常捕获等.程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,

因此程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域。

元空间

上面说到,jdk1.8中,已经不存在永久代(方法区),替代它的一块空间叫做“元空间”,和永久代类似,都是JVM规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的大小

什么是垃圾即如何判断对象已死?

内存中没引用的对象称之为垃圾。

判断对象是否已死:

(1)引用计数算法:给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方不再引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象。

(2)可达性分析算法:当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的,是死对象。如下图:object1、object2、object3、object4和GC Roots之间有可达路径,这些对象不会被回收,但object5、object6、object7到GC Roots之间没有可达路径,这些对象就被判了死刑。

(3)方法区回收:上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。 判断常量是否废弃可以判断是否有地方引用这个常量,如果没有引用则为废弃的常量。 判断类是否废弃需要同时满足如下条件:

1该类所有的实例已经被回收(堆中不存在任何该类的实例)

2加载该类的ClassLoader已经被回收

3该类对应的java.lang.Class对象在任何地方没有被引用(无法通过反射访问该类的方法)

垃圾回收的算法有哪些?,他们有什么特点呢?

常用的垃圾回收算法有三种:标记-清除算法、复制算法、标记-整理算法。

(1)标记-清除算法:分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象

缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

(2)标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

缺点:实际可使用的内存空间缩小为原来的一半,比较适合。

(3)标记-整理算法:先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存。

(4)分代收集算法:把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

在这些区域的垃圾回收大概有如下几种情况: 新生代使用时minor gc 老年代使用的full gc

一般来说,我们把新生代的回收称为Minor GC,Minor意思是次要的,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短。而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用标记-整理算法,这种GC每次都比较慢,造成的暂停时间比较长,通常是Minor GC时间的10倍以上。 所以很明显,我们需要尽量通过Minor GC来回收内存,而尽量少的触发Full GC。毕竟系统运行一会儿就要因为GC卡住一段时间,再加上其他的同步阻塞,整个系统给人的感觉就是又卡又慢。

什么时间回收垃圾?

当程序运行时,各种数据、对象、线程、内存等都时刻在发生变化,当下达垃圾收集命令后就立刻进行收集吗?肯定不是。这里来了解两个概念:安全点(safepoint)和安全区(safe region)。

安全点基本上以“是否具有让程序长时间执行的特征”为标准进行选定。

安全区就是在一段代码片段中,引用关系不会发生变化,可以看作是被扩展、拉长了的安全点。

常见垃圾收集器

现在常见的垃圾收集器有如下几种:

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、CMS、Parallel Old

堆内存垃圾收集器:G1----->G1收集器在有限的时间内可以获得最大的回收效率。

新生代和老年代的垃圾回收有什么不同?

分代收集算法(Generational Collection)

分代回收算法实际上是把复制算法和标记整理法的结合,并不是真正一个新的算法,一般分为:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。

新生代:由于新生代产生很多临时对象,大量对象需要进行回收,所以采用复制算法是最高效的。

老年代:回收的对象很少,都是经过几次标记后都不是可回收的状态转移到老年代的,所以仅有少量对象需要回收,故采用标记清除或者标记整理算法。