Java基础
面向对象
包装:包装的意义在于清楚地识别允许外部使用的所有成员函数和数据项。内部细节对外部呼叫透明,外部呼叫不需要修改或关心内部实现
继承:继承基本类别的方法,并做出自己的改变和/或扩展。子类的共同方法或属性直接使用父类,而无需重新定义,只需扩展个性化
多态:根据对象所属类别的不同,外部调用同一方法,实际执行逻辑不同
JDK、JRE、JVM之间的区别
JDK:Java Develpment Kit java 开发工具
JRE:Java Runtime Environment java运行时环境
JVM:java Virtual Machine java 虚拟机
==和equals方法前的区别
==:对比栈中的值,基本数据类型为变量值,引用类型为堆中内存对象的地址
equals:object也采用了中默认==通常会重写比较
hashCode()与equals()之间的关系
HashCode介绍:hashCode() 它的功能是获取哈希码,也称为散列码;它实际上是回到一个int整数。哈希码的作用是确定哈希表中对象的索引位置。hashCode() 定义在JDK的Object.java中,Java任何类别都包含在内hashCode() 函数。
存储在散列表中的键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
若两个对象相等,则hashcode一定是一样的
两个对象相等,分别调用两个对象equals方法都返回true
两个对象相同hashcode它们不一定相等
因此,equals如果方法被覆盖,则hashCode还必须覆盖方法
hashCode()默认行为对堆上的对象具有独特的价值。如果不重写hashCode(),则该class无论如何,两个对象都不会相等(即使两个对象指向相同的数据)
final关键词的作用是什么?
修饰:表示类不能继承
修饰方法:表示方法不能被子类覆盖,但可以重载
修改变量:表示变量一旦赋值,就不能改变其值。
装饰成员变量:
如果final类变量只能在静态初始化块中指定初始值或声明该类变量时指定。
如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
局部变量修改:系统不初始化局部变量,局部变量必须由程序员显示。因此,使用final修改局部变量时,可以在定义时指定默认值(后面的代码不能再赋予变量),也可以在后面的代码中指定默认值final变量赋初值(只有一次)
修改基本类型数据和引用类型数据:
若是基本数据类型的变量,其数值一旦初始化就不能改变;
如果是引用类型的变量,在初始化后不能指向另一个对象。但引用值是可变的。
String、StringBuffer、StringBuilder的区别
String它是不可改变的,如果试图修改,将生成一个新的字符串对象,StringBuffer和StringBuilder是可变的
StringBuffer线程安全,StringBuilder线程不安全,所以在单线程环境下StrngBuilder效率会更高
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
接口和抽象类的区别
抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法。
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
抽象类只能继承一个,接口可以实现多个。
List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素
ArrayList和LinkedList区别
首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用
HashMap和HashTable有什么区别?其底层实现是什么?
区别 :
HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
HashMap允许key和value为null,而HashTable不允许
底层实现
计算key的hash值,二次hash然后对数组长度取模,对应到数组下标
如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组
如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
key为null,存在下标0的位置
谈谈ConcurrentHashMap的扩容机制
1.7版本
1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
2. 每个Segment相对于一个小型的HashMap
3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4. 先生成新的数组,然后转移元素到新数组中
5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1. 1.8版本的ConcurrentHashMap不再基于Segment实现
2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4. ConcurrentHashMap是支持多个线程同时扩容的
5. 扩容之前也先生成一个新的数组
6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
Jdk1.7到Jdk1.8 HashMap 发生了什么变化(底层)?
1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
说一下HashMap的Put方法
1. 根据Key通过哈希算法与与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
1. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
2. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
1. 如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
2. 如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树
3. 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法
泛型中extends和super的区别
1. <? extends T>表示包括T在内的任何T的子类
2. <? super T>表示包括T在内的任何T的父类
深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
2. 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
HashMap的扩容机制原理
1.7版本
1.8版本
CopyOnWriteArrayList的底层原理是怎样的
1. 首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
3. 写操作结束之后会把原数组指向新数组
4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
什么是字节码?采用字节码的好处是什么?
Java中的编译器和解释器:Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
采用字节码的好处:Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java中的异常体系是怎样的
Java中的所有异常都来自顶级父类Throwable。
Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
Java中有哪些类加载器
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。
说说类加载器双亲委派模型
1. BootstrapClassLoader
2. ExtClassLoader
3. AppClassLoader
AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader。
JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。
GC如何判断对象可以被回收
引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,
可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
GC Roots的对象有:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
JVM中哪些是线程共享区
线程共享
堆区
方法区
线程独有
栈
本地方法栈
程序计数器
JVM参数有哪些?
JVM参数大致可以分为三类:
1. 标注指令: -开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。
2. 非标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X 打印出来。
3. 不稳定参数: -XX 开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。
4. 在JDK1.8版本下,有几个常用的不稳定指令:
1. java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。
2. java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。
3. java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终生效的实际值。
JVM有哪些垃圾回收算法?
1. MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
2. Copying 拷贝算法:为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存货对象的个数有关。
3. MarkCompack 标记压缩算法:为了解决拷贝算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将端边界以外的所有内存直接清除。
JVM有哪些垃圾回收器?
新生代收集器:
Serial
ParNew
Parallel Scavenge
老年代收集器:
CMS
Serial Old
Parallel Old
整堆收集器:
G1
垃圾回收分为哪些阶段
第一:初始标记 标记出GCRoot直接引用的对象。STW
第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。
第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。
第四:重新标记: 跟CMS中的重新标记过程是差不多的。
第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
什么是STW?
STW: Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
什么是三色标记?
三色标记:是一种逻辑上的抽象。将每个内存对象分成三种颜色:
1. 黑色:表示自己和成员变量都已经标记完毕。
2. 灰色:自己标记完了,但是成员变量还没有完全标记完。
3. 白色:自己未标记完。
怎么确定一个对象到底是不是垃圾?
1. 引用计数: 这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
2. 根可达算法: 这种方式是在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾。
一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1. 用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。
2. JVM要实例化一个对象,首先要在堆当中先创建一个对象。-> 半初始化状态
3. 对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。-> 多大年龄才会移入老年代? 年龄最大15, 超过一定年龄后,对象转入老年代。
4. 当方法执行结束后,栈中的指针会先移除掉。
5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
你们项目如何排查JVM问题
对于还在正常运行的系统:
1. 可以使用jmap来查看JVM中各个区域的使用情况
2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
4. 通过各个命令的结果,或者jvisualvm等工具来进行分析
5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统:
1. 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2. 我们可以利用jsisualvm等工具来分析dump文件
3. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4. 然后再进行详细的分析和调试
总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
Java并发
线程的生命周期?线程有几种状态
线程通常有五种状态:
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
阻塞的情况又分为三种:
1. 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
3. 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法
sleep()、wait()、join()、yield()之间的的区别
1. sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2. sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5. sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6. sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
7. yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
8. join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
对线程安全的理解
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问,当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
Thread和Runable的区别
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。 应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
ThreadLocal的底层原理
1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
并发、并行、串行之间的区别
1. 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
2. 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
3. 并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行
并发的三大特性
原子性:原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
Java死锁如何避免?
造成死锁的几个原因:
1. 一个资源每次只能被一个线程使用
2. 一个线程在阻塞等待某个资源时,不释放已占有资源
3. 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4. 若干线程形成头尾相接的循环等待资源关系
5. 这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
2. 要注意加锁时限,可以针对所设置一个超时时间
3. 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
为什么用线程池?解释下线程池参数?
降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
`corePoolSize` 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
`maxinumPoolSize` 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
`keepAliveTime`、`unit` 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 `setKeepAliveTime` 来设置空闲时间
`workQueue` 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
`ThreadFactory` 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
`Handler` 任务拒绝策略,有两种情况,第一种是当我们调用`shutdown` 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝
线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
1. 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源