java基础面试题
1、 ==和equals⽅法之前的区别
==:对⽐的是栈中的值,基本数据类型是变量值,引⽤类型是堆中内存对象的地址
equals:object中默认也是采⽤==⽐较,通常会重写
2、 hashCode()与equals() 关系和区别
如果两个对象相等,则hashcode⼀定也是相同的
两个对象相等,对两个对象分别调⽤equals⽅法都返回true
两个对象有相同的hashcode值,它们也不⼀定是相等的
因此,equals⽅法被覆盖过,则hashCode⽅法也必须被覆盖hashCode()的默认⾏为是对堆上的对象产⽣独特值。如果没有重写hashCode(),则该class的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)
3、 final关键字的作⽤是什么?
修饰类:表示类不可被继承
修饰⽅法:表示⽅法不可被⼦类覆盖,但是可以重载
修饰变量:表示变量⼀旦被赋值就不可以更改它的值。
修饰成员变量:
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
如果final修饰的是成员变量,可以在⾮静态初始化块、声明该变量或者构造器中执⾏初始值。
修饰局部变量:
系统不会为局部变量进⾏初始化,局部变量必须由程序员显示初始化。因此使⽤final修饰局部变量时,即可以在定义时指定默认值(后⾯的代码不能对变量再赋值),也可以不指定默认值,⽽在后⾯的代码中对final变量赋初值(仅⼀次)
修饰基本类型数据和引⽤类型数据:
如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。但是引⽤的值是可变的。
4、 String、StringBuffer、StringBuilder的区别
String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是可变的
StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更⾼
5、重载和重写的区别
重载:发⽣在同⼀个类中,⽅法名必须相同,参数类型不同、个数不同、顺序不同,⽅法返回值和访问修饰符可以不同,发⽣在编译时。
重写:发⽣在⽗⼦类中,⽅法名、参数列表必须相同,返回值范围⼩于等于⽗类,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类;如果⽗类⽅法访问修饰符为private则⼦类就不能重写该⽅法。
6、常见访问修饰符
1、private(私有):private修饰的属性和方法,不能被其他到类访问,也不能被子类继承和访问,只能在当前类访问。
2、default (缺省):没有加修饰符的属性和方法,同一个包的其他类可访问和继承。
3、protected(受保护的):被其修饰的属性和方法,同一个包的其他类可访问和继承,或者不同包的其他子类可访问。
4、public(公有的):不存在访问权限,全部类都可以访问。
7、接⼝和抽象类的区别
象类可以存在普通成员函数,⽽接⼝中只能存在public abstract ⽅法。
抽象类中的成员变量可以是各种类型的,⽽接⼝中的成员变量只能是public static final类型的。
抽象类只能继承⼀个,接⼝可以实现多个。
当你关注⼀个事物的本质的时候,⽤抽象类;当你关注⼀个操作的时候,⽤接⼝
8、List和Set的区别
List: 有序,按对象进⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使⽤Iterator取出所有元素,在逐⼀遍历,还可以使⽤get(int index)获取指定下标的元素
Set: ⽆序,不可重复,最多允许有⼀个Null元素对象,取元素时只能⽤Iterator接⼝取得所有元素,在逐⼀遍历各个元素
9、ArrayList和 LinkedList 区别
底层结构不同。
- ArrayList
基于数组实现, 线程不安全。 插入的话会默认插到最后一位。 而且还要插入的时候考虑索引和数组大小。(例如自动扩容)
默认容量为 10 (DEFAULT_CAPACITY)
在用户向ArrayList追加对象时,Java总是要先计算容量(Capacity)是否适当,若容量不足则把原数组拷贝到以指定容量为长度创建的 新数组内,并对原数组变量重新赋值,指向新数组。
在这同时,size进行自增1。在删除对象时,先使用拷贝方法把指定index后面的对象前移1位(如果 有的话),然后把空出来的位置置null,交给Junk收集器销毁,size自减1。 - LinkedList
基于链表实现
使用场景不同
如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;
不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
10、HashMap 和 HashTable 有什么区别?其底层实现是什么?
安全性不同
- HashMap
无 synsynchronized 修饰。 现场不安全 - HashTable
线程安全的
插入值的允许度不同
- HashMap
允许 key、 value 为空 - HashTable
不允许 key、 value 为空
底层实现
- 数组+链表实现,jdk8开始链表⾼度到8、数组⻓度超过64,链表转变为红⿊树,元素以内部类Node节点存在
- 计算key的hash值,⼆次hash然后对数组⻓度取模,对应到数组下标,
- 如果没有产⽣hash冲突(下标位置没有元素),则直接创建Node存⼊数组,
- 如果产⽣hash冲突,先进⾏equal⽐较,相同则取代该元素,不同,则判断链表⾼度插⼊链表,链表⾼度达到8,并且数组⻓度到64则转变为红⿊树,⻓度低于6则将红⿊树转回链表
- key为null,存在下标0的位置
11、谈 ConcurrentHashMap 的扩容机制
- 1.7
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相对于⼀个⼩型的HashMap
- 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
- 先⽣成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
- 1.8
- 1.8版本的ConcurrentHashMap不再基于Segment实现
- 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
- 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进⾏扩容
- ConcurrentHashMap是⽀持多个线程同时扩容的
- 扩容之前也先⽣成⼀个新的数组
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作
12、Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率
- 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
- 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法,节省CPU资源
13、泛型中 extends 和 super 的区别
子类 和 父类的区别
- extends T>表示包括T在内的任何T的⼦类
- super T>表示包括T在内的任何T的⽗类
14、深拷⻉和浅拷⻉
深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。
浅拷贝不会存在新的内存地址,指向的实际是同一个内存地址。 深拷贝会对引用地址也进行赋值。实际在内存中的应用地址是不同的。
- 浅拷⻉: 只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
- 深拷⻉: 既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制,深拷⻉出来的对象,内部的属性指向的不是同⼀个对象
15、CopyOnWriteArrayList 的相关解释说明
- ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
- 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
- 写操作结束之后会把原数组
- CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很⾼的场景
16、java 中的异常体系
- java中的所有异常都来⾃顶级⽗类Throwable。
- Throwable下有两个⼦类Exception和Error。
- Error是程序⽆法处理的错误,⼀旦出现这个错误,则程序将被迫停⽌运⾏。
- Exception不会导致程序停⽌,⼜分为两个部分RunTimeException运⾏时异常和CheckedException检查异常。
- RunTimeException常常发⽣在程序运⾏过程中,会导致程序当前线程执⾏失败。
- CheckedException常常发⽣在程序编译过程中,会导致程序编译不通过。
17、 java 中的类加载器
jDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
- BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class⽂件。
- ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的jar包和class类。
- AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。
18、 说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
- AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会先使⽤ExtClassLoader的loadClass⽅法来加载类,
同样ExtClassLoader的loadClass⽅法中会先使⽤BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,
如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰进⾏加载。
19、 GC如何判断对象可以被回收
- 引⽤计数法:每个对象有⼀个引⽤计数属性,新增⼀个引⽤时计数加1,引⽤释放时计数减1,计数为0时可以回收
注意互相引用时, 引用计数法无法回收、 (例如 A 引用 B, B 引用 A。则他们互相引用计数为 1,无法被该机制回收) - 可达性分析法:从 GC Roots 开始向下搜索,搜索所⾛过的路径称为引⽤链。当⼀个对象到 GCRoots 没有任何引⽤链相连时,则证明此对象是不可⽤的,那么虚拟机就判断是可回收对象。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引⽤的对象
- ⽅法区中类静态属性引⽤的对象
- ⽅法区中常量引⽤的对象
- 本地⽅法栈中JNI(即⼀般说的Native⽅法)引⽤的对象
可达性算法中的不可达对象并不是⽴即死亡的,对象拥有⼀次⾃我拯救的机会。对象被系统宣告死亡⾄少要经历两次标记过程:第⼀次是经过可达性分析发现没有与GC Roots相连接的引⽤链,第⼆次是在由虚拟机⾃动建⽴的Finalizer队列中判断是否需要执⾏finalize()⽅法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize⽅法,若未覆盖,则直接将其回收。否则,若对象未执⾏过finalize⽅法,将其放⼊F-Queue队列,由⼀低优先级线程执⾏该队列中对象的finalize⽅法。执⾏finalize⽅法完毕后,GC会再次判断该对象是否可达,若不可达,则进⾏回收,否则,对象“复活”每个对象只能触发⼀次finalize()⽅法
由于finalize()⽅法运⾏代价⾼昂,不确定性⼤,⽆法保证各个对象的调⽤顺序,不推荐⼤家使⽤,建议遗忘它。
20、 线程共享区
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
21、 jvm 相关参数 和 排查方向
env 中 XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base 添加配置项监控 oom
jprofiler 工具在线监控。
wireshark 查看 dump 文件
22、 ⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- ⽤户创建⼀个对象,JVM⾸先需要到⽅法区去找对象的类型信息。然后再创建对象。
- JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态
- 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。
- 当⽅法执⾏结束后,栈中的指针会先移除掉。
- 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
23、 怎么确定一个对象是否是垃圾
- 引⽤计数法
通过引用进行计数,当没有计数时认为无引用 - 根可达算法
从引用对象向下寻找。找不到对象则认为是垃圾
24、JVM有哪些垃圾回收算法?
MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
Copying 拷⻉算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了拷⻉算法。拷⻉算法将内存分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽且,他的效率跟存货对象的个数有关。
MarkCompack 标记压缩算法:为了解决拷⻉算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将端边界以外的所有内存直接清除。
25、 jvm 的垃圾回收器
- 新⽣代收集器:
Serial
ParNew
Parallel Scavenge - ⽼年代收集器:
CMS
Serial Old
Parallel Old - 整堆收集器:
G1
26、 垃圾回收阶段
- 初始标记 标记出GCRoot直接引⽤的对象。STW
- 标记Region,通过RSet标记出上⼀个阶段标记的Region引⽤到的Old区Region。
- 并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,⽽只需要遍历第⼆步标记出来的Region。
- 重新标记: 跟CMS中的重新标记过程是差不多的。
- 垃圾清理:与CMS不同的是,G1可以采⽤拷⻉算法,直接将整个Region中的对象拷⻉到另
⼀个Region。⽽这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
27、什么是三⾊标记?
三⾊标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜⾊:
- ⿊⾊:表示⾃⼰和成员变量都已经标记完毕。
- 灰⾊:⾃⼰标记完了,但是成员变量还没有完全标记完。
- ⽩⾊:⾃⼰未标记完。
28、JVM参数有哪些?
JVM参数⼤致可以分为三类:
- 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
- ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
- 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。详细的⽂档
资料⾮常少。在JDK1.8版本下,有⼏个常⽤的不稳定指令:
java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。
java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。
java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终⽣效的实际值。
29、 线程的⽣命周期?线程有⼏种状态
线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:
- 新建状态(New):新创建了⼀个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调⽤了该对象的start⽅法。该状态的线程位于可运⾏线程池中,变得可运⾏,等待获取CPU的使⽤权。
- 运⾏状态(Running):就绪状态的线程获取了CPU,执⾏程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使⽤权,暂时停⽌运⾏。直到线程进⼊就绪状态,才有机会转到运⾏状态。
- 死亡状态(Dead):线程执⾏完了或者因异常退出了run⽅法,该线程结束⽣命周期。
阻塞的情况⼜分为三种:
- 等待阻塞:运⾏的线程执⾏wait⽅法,该线程会释放占⽤的所有资源,JVM会把该线程放⼊“等待池”中。进⼊这个状态后,是不能⾃动唤醒的,必须依靠其他线程调⽤notify或notifyAll⽅法才能被唤醒,wait是object类的⽅法
- 同步阻塞:运⾏的线程在获取对象的同步锁时,若该同步锁被别的线程占⽤,则JVM会把该线程放⼊“锁池”中。
- 其他阻塞:运⾏的线程执⾏sleep或join⽅法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终⽌或者超时、或者I/O处理完毕时,线程重新转⼊就绪状态。sleep是Thread类的⽅法
30、并发、并⾏、串⾏之间的区别
- 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
- 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
- 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏
31、sleep()、wait()、join()、yield()之间的的区别
锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进⼊就绪队列进⾏等待cpu资源分配。
等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中
- sleep 是 Thread 类的静态本地⽅法,wait 则是 Object 类的本地⽅法。
- sleep⽅法不会释放lock,但是wait会释放,⽽且会加⼊到等待队列中。
- sleep⽅法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别⼈中断)。
- sleep ⼀般⽤于当前线程休眠,或者轮循暂停操作,wait 则多⽤于多线程之间的通信。
- sleep 会让出 CPU 执⾏时间且强制上下⽂切换,⽽ wait 则不⼀定,wait 后可能还是有机会重新竞争到锁继续执⾏的。
- yield()执⾏后线程直接进⼊就绪状态,⻢上释放了cpu的执⾏权,但是依然保留了cpu的执⾏资格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏
- join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队列,直到线程A结束或中断线程
31、 对线程安全的理解
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问,当多个线程访问⼀个对象时,如果不⽤进⾏额外的同步控制或其他的协调操作,调⽤这个对象的⾏为都可以获得正确的结果,我们就说这个对象是线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是⽤户分配的空间。堆在操作系统对进程初始化的时候分配,运⾏过程中也可以向系统要额外的堆,但是⽤完了要还给操作系统,要不然就是内存泄漏。在Java中,堆是Java虚拟机所管理的内存中最⼤的⼀块,是所有线程共享的⼀块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
栈是每个线程独有的,保存其运⾏状态和局部⾃动变量的。栈在线程开始的时候初始化,每个线程的栈互相独⽴,因此,栈是线程安全的。操作系统在切换线程的时候会⾃动切换栈。栈空间不需要在⾼级语⾔⾥⾯显式的分配和释放。
在每个进程的内存空间中都会有⼀块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
32、Thread和Runable的区别
Thread和Runnable的实质是继承关系,没有可⽐性。⽆论使⽤Runnable还是Thread,都会newThread,然后执⾏run⽅法。
⽤法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执⾏⼀个任务,那就实现runnable。
33、 并发的三⼤特性
原子性
关键字:synchronized原⼦性是指在⼀个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执⾏完成,要不都不执⾏。
就好⽐转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。
可见性
关键字:volatile、synchronized、final当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2⼜使⽤了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可⻅性问题。
有序性
关键字:volatile、synchronized虚拟机在进⾏代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不⼀定会按照我们写的代码的顺序来执⾏,有可能将他们重排序。实际上,对于有些代码进⾏重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
34、ThreadLocal的底层原理
- ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意⽅法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
- 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收,Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
- ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)
35、Java死锁如何避免?
- 造成死锁的⼏个原因:
- ⼀个资源每次只能被⼀个线程使⽤
- ⼀个线程在阻塞等待某个资源时,不释放已占有资源
- ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
- 若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
- 要注意加锁时限,可以针对所设置⼀个超时时间
- 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
36、如何理解volatile关键字
保证被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共享变量的值,新值总是可以被其他线程⽴即得知。
但是不能保证线程安全
37、线程池使用以及相关参数
使用原因
- 降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。
- 提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。
- 提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。
相关参数
- corePoolSize
代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不会消除,⽽是⼀种常驻线程 - maxinumPoolSize
代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最⼤线程数 - keepAliveTime、 unit
表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程不会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间 - workQueue
⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程 - ThreadFactory
实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚ - Handler
任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池后,这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处理新提交的任务时,这是也就拒绝
- corePoolSize
38、线程池的底层⼯作原理
- 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
- 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数