java -> 基础面试题


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节点存在
  1. 计算key的hash值,⼆次hash然后对数组⻓度取模,对应到数组下标,
  2. 如果没有产⽣hash冲突(下标位置没有元素),则直接创建Node存⼊数组,
  3. 如果产⽣hash冲突,先进⾏equal⽐较,相同则取代该元素,不同,则判断链表⾼度插⼊链表,链表⾼度达到8,并且数组⻓度到64则转变为红⿊树,⻓度低于6则将红⿊树转回链表
  4. key为null,存在下标0的位置

11、谈 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. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作

12、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资源

13、泛型中 extends 和 super 的区别

子类 和 父类的区别

  1. 表示包括T在内的任何T的⼦类
  2. 表示包括T在内的任何T的⽗类

14、深拷⻉和浅拷⻉

深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。

浅拷贝不会存在新的内存地址,指向的实际是同一个内存地址。 深拷贝会对引用地址也进行赋值。实际在内存中的应用地址是不同的。

  1. 浅拷⻉: 只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
  2. 深拷⻉: 既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制,深拷⻉出来的对象,内部的属性指向的不是同⼀个对象

15、CopyOnWriteArrayList 的相关解释说明

  1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
  2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
  3. 写操作结束之后会把原数组
  4. 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中存在三个默认的类加载器:

  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进⾏加载,如果没加载到才由⾃⼰进⾏加载。

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清除,都经历了什么过程?

  1. ⽤户创建⼀个对象,JVM⾸先需要到⽅法区去找对象的类型信息。然后再创建对象。
  2. JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态
  3. 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。
  4. 当⽅法执⾏结束后,栈中的指针会先移除掉。
  5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

23、 怎么确定一个对象是否是垃圾

  • 引⽤计数法
    通过引用进行计数,当没有计数时认为无引用
  • 根可达算法
    从引用对象向下寻找。找不到对象则认为是垃圾

24、JVM有哪些垃圾回收算法?

  1. MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。

  2. Copying 拷⻉算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了拷⻉算法。拷⻉算法将内存分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽且,他的效率跟存货对象的个数有关。

  3. MarkCompack 标记压缩算法:为了解决拷⻉算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将端边界以外的所有内存直接清除。

25、 jvm 的垃圾回收器

  • 新⽣代收集器:
    Serial
    ParNew
    Parallel Scavenge
  • ⽼年代收集器:
    CMS
    Serial Old
    Parallel Old
  • 整堆收集器:
    G1

26、 垃圾回收阶段

  1. 初始标记 标记出GCRoot直接引⽤的对象。STW
  2. 标记Region,通过RSet标记出上⼀个阶段标记的Region引⽤到的Old区Region。
  3. 并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,⽽只需要遍历第⼆步标记出来的Region。
  4. 重新标记: 跟CMS中的重新标记过程是差不多的。
  5. 垃圾清理:与CMS不同的是,G1可以采⽤拷⻉算法,直接将整个Region中的对象拷⻉到另

⼀个Region。⽽这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

27、什么是三⾊标记?

三⾊标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜⾊:

  1. ⿊⾊:表示⾃⼰和成员变量都已经标记完毕。
  2. 灰⾊:⾃⼰标记完了,但是成员变量还没有完全标记完。
  3. ⽩⾊:⾃⼰未标记完。

28、JVM参数有哪些?

  • JVM参数⼤致可以分为三类:

    1. 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
    2. ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
    3. 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。详细的⽂档
  • 资料⾮常少。在JDK1.8版本下,有⼏个常⽤的不稳定指令:
    java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。
    java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。
    java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终⽣效的实际值。

29、 线程的⽣命周期?线程有⼏种状态

  • 线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:

    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类的⽅法

30、并发、并⾏、串⾏之间的区别

  • 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
  • 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
  • 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏

31、sleep()、wait()、join()、yield()之间的的区别

锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进⼊就绪队列进⾏等待cpu资源分配。

等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中

  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结束或中断线程

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的底层原理

  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经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)

35、Java死锁如何避免?

  • 造成死锁的⼏个原因:
    1. ⼀个资源每次只能被⼀个线程使⽤
    2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
    3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
    4. 若⼲线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决

36、如何理解volatile关键字

保证被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共享变量的值,新值总是可以被其他线程⽴即得知。

但是不能保证线程安全

37、线程池使用以及相关参数

  • 使用原因

    1. 降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。
    2. 提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。
    3. 提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。
  • 相关参数

    • corePoolSize
      代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不会消除,⽽是⼀种常驻线程
    • maxinumPoolSize
      代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最⼤线程数
    • keepAliveTime、 unit
      表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程不会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间
    • workQueue
      ⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程
    • ThreadFactory
      实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚
    • Handler
      任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池后,这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处理新提交的任务时,这是也就拒绝

38、线程池的底层⼯作原理

  1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
  3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数