Java面试题(第二天)简易总结版

前言

首先,我个人比较懒,上午一般摸摸鱼,不喜欢学习,今天这么早的原因是,昨天发布的第一天Java面试题的简易总结版本,收到了一位小伙伴的喜欢,让我深感荣幸,为了不辜负他的期待,我今天早早的就准备开始了,非常感谢你的支持,因为你的期待让我更加有动力的学习和写笔记,希望我们共同努力,都能变得更强,更好。加油。

ArrayList和LinkedList

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机)。扩容机制(及时扩大内存的方式):因为数组长度的固定的,超过原来的长度就要重新建一个新的数组,然后把老的数据拷贝过来,如果不是在数组的最后插入数据(尾插法)的话,还会涉及元素的移动,所以使用尾插法可以指定初始容量,可以提供新能,甚至超过linkedList(它需要创建大量的node。节点对象)。

LinkedList:基于链表,分散内存,适合做数据的插入,不适合查询,查询时要逐一遍历判断。遍历是不能for循环,要用迭代器(iterator),因为for循环消耗很大新能(每次get的时候都要重新遍历),而且不要试图使用indexOf返回元素索引(如果结果为空,会遍历整个列表)。

HashMap 和HashTable

区别?

  • HashMap 方法没有synchronize修饰, 线程不安全,但是HashTable安全。
  • HashMap允许key,value为null,但是HashTable不允许。

HashMap的底层实现?

  • 数组+链表
  • jdk8开始,链表高度到8的时候,链表就转变成为红黑树,元素以内部类Node节点存在。
    在这里插入图片描述

ConcurrentHashMap

这个好难哦,感觉记不住,自己背吧,没办法总结。 我也背。。。

JDK7:
在这里插入图片描述
JDK8:
在这里插入图片描述

字节码

什么是字节码?

Java中引入虚拟机的概念,编译器只需要面向虚拟机,生成虚拟机可以理解的代码,然后由解释器讲虚拟机代码转化成为特定的机器码执行,在Java中,虚拟机可以理解的代码就是 字节码。

好处?
使用字节码之后,代码变得跨平台和可移植性就比较高了,一定程度上解决了传统的解释性语言效率低的问题。Java程序可以做到:一次编译,处处运行。

异常

直接结论:
在这里插入图片描述

Java类加载器

直接结论:
在这里插入图片描述

双亲委派模型

在这里插入图片描述
好处?

  • 为了安全性,避免自己写的动态类替换了Java的核心类,比如String(简单来说,就是你自己定义了一个跟Java核心类的同名类,那你要用的时候,比如用String,他肯定还有优先找系统的类,而不是你自己定义的。)
  • 避免类的重复加载,因为JVM区分不同的类,不仅仅是根据类名,相同的class文件被不同的加载器加载也是不同的类。

GC 如何判断对象可以被回收

  • 引用计数法:每个对象都有一个计数器,新增引用+1,释放-1,为0时就回收。(缺点是:AB如果互相引用,那么计数器永远不可能为0,那就不会被回收)
  • 可达性分析:从GC Roots开始向下搜索,搜索走过的路叫做引用连,当它不跟其他引用连相连时,说明他没人用,也就是可回收对象。

GCRoots的对象:

  • 虚拟机栈中引用的对象
  • 方法区中类静态熟悉引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈引用的对象

补充:

在这里插入图片描述

final finally finalize

final:
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个 常量不能被重新赋值。
finally:
一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码放入finally代码块中,表示不管是 否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize:
是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调 用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

线程

线程状态:
创建 ,就绪,运行,阻塞,死亡 五种状态。

阻塞又分为三种:

  • 等待阻塞:线程执行wait方法,它就休眠了,释放自己占用的资源,JVM把它放在等待池中(等待被唤醒:notify或者notifyAll)。wait是object的方法。
  • 同步阻塞:运行的线程在获取对象的同步锁是,若同步锁被别人占用,JVM会把它放在锁池里面。(等待,同步锁出来,把自己带走,所以是锁池)。
  • 其他阻塞:运行的线程调用sleep或者join方法,或者发出来IO请求,JVM会把改线程设置为阻塞状态。当sleep超时,Join等待线程终止或者超时,或者IO处理完毕,线程重新转为就绪状态。

状态:

  • 新建(New):新创建了一个线程对象
  • 就绪(Runnable):线程创建以后,调用了start方法,该线程位于可运行线程池中,变得可运行,等待获得CPU的使用权。
  • 运行(Running):就绪状态的线程获得了CPU,执行代码。
  • 阻塞(Blocked):阻塞状态是因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转入运行状态。
  • 死亡(Dead):线程执行完了或者异常退出run,该线程结束生命周期。

sleep()、wait()、join()、yield()

关于锁池?

  • 所有需要竞争同步锁的线程都放在锁池中(上文出现过,在同步阻塞那个地方,还记得吗?),比如当前对象的锁被其中一个线程所得到,则其他现在需要在这个锁池进行等待,而前面的线程释放同步锁后,锁池中其他宣城就去竞争,而当某个线程得到之后就会进入就绪队列。
    关于等待池?
  • 当我们调用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 后可能还是有机会重新竞
    争到锁继续执行的。

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,
所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
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) 或者在任何情况下,程序结束时,这个线
程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要
正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都
是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。

thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个
IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作
的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线
程就不能用Java的线程池。

ThreadLocal

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所
有ThreadLocal对象及其对应的值。

ThreadLocalMap 由一个个 Entry 对象构成。

Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构
成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该
key就会被垃圾收集器回收。

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对
象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在
线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。

Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的
connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种
隔离
在这里插入图片描述

ThreadLocal内存泄露原因,如何避免?

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露
堆积后果很严重,无论多少内存,迟早会被占光,
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,
Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的
connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种
隔离
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时
间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用
java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal
实例,value为线程变量的副本。

在这里插入图片描述
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,
Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强
引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这
些key为null的Entry的value就会一直存在一条强引用链(红色链条)

key 使用强引用
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强
引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱
引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用
set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有
手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任
  • 何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

并发、并行、串行的区别

串行:时间上不能重叠,前一个没搞定,下一个任务就只能等着(理解记忆为串珠子,一个一个的。)
并行:时间上是重叠的,两个任务在同一时刻,互不干扰的同时执行。(理解为一并进行)
并发:允许两个任务彼此干扰,同一个时间点,只有一个任务,但是可以交替执行。

并发的三大特性

  • 原子性
    原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要
    不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,
    往账户B加上1000元。2个操作必须全部完成。
    那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,
    包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增
    完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是
    一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

  • 可见性
    当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定
    还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

  • 有序性
    虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按
    照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对
    变量的值没有造成影响,但有可能会出现线程安全问题。

volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许
一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或
java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。
我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)。

线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的
一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对
Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去
执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就
是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就
将所有任务的 run 方法串联起来。

基本数据类型 包装类

基本类型

byte/8
char/16
short/16
int/32
float/32
long/64
double/64
boolean/~

boolean 只有两个值:true、false,可以使⽤ 1 bit 来存储,但是具体⼤⼩没有明确规定。JVM 会在编
译时期将 boolean 类型的数据转换为 int,使⽤ 1 来表示 true,0 表示 false。JVM ⽀持 boolean 数组,
但是是通过读写 byte 数组来实现的。

包装类型

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使⽤⾃动装箱与拆箱完成。

Integer x = 2; // 装箱 调⽤了 Integer.valueOf(2)
int y = x; // 拆箱 调⽤了 X.intValue().

自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
Java使用自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提高了效率,由编译器来完成,编译器会在编译期根据语法决定是否进行装箱和拆箱动作。

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建⼀个对象;
  • Integer.valueOf(123) 会使⽤缓存池中的对象,多次调⽤会取得同⼀个对象的引⽤。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 -
128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=
来指定这个缓冲池的⼤⼩,该选项在 JVM 初始化的时候会设定⼀个名为
java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上

String

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
在 Java 8 中,String 内部使⽤ char 数组存储数据。
在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串,同时使⽤ coder 来标识使⽤了哪种编
码。
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引⽤其它数组。并且 String 内部没
有改变 value 数组的⽅法,因此可以保证 String 不可变。

不可变的好处?

  1. 可以缓存 hash 值
    因为 String 的 hash 值经常被使⽤,例如 String ⽤做 HashMap 的 key。不可变的特性可以使得 hash
    值也不可变,因此只需要进⾏⼀次计算。

  2. String Pool 的需要
    如果⼀个 String 对象已经被创建过了,那么就会从 String Pool 中取得引⽤。只有 String 是不可变的,
    才可能使⽤ String Pool。

  3. 安全性
    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为⽹络连接参数的情况下如果
    String 是可变的,那么在⽹络连接过程中,String 被改变,改变 String 的那⼀⽅以为现在连接的是其它
    主机,⽽实际情况却不⼀定是。

  4. 线程安全
    String 不可变性天⽣具备线程安全,可以在多个线程中安全地使⽤。

String Pool

字符串常量池(String Pool)保存着所有字符串字⾯量(literal strings),这些字⾯量在编译时期就确
定。不仅如此,还可以使⽤ String 的 intern() ⽅法在运⾏过程将字符串添加到 String Pool 中。
当⼀个字符串调⽤ intern() ⽅法时,如果 String Pool 中已经存在⼀个字符串和该字符串值相等(使⽤
equals() ⽅法进⾏确定),那么就会返回 String Pool 中字符串的引⽤;否则,就会在 String Pool 中添
加⼀个新的字符串,并返回这个新字符串的引⽤

下⾯示例中,s1 和 s2 采⽤ new String() 的⽅式新建了两个不同字符串,⽽ s3 和 s4 是通过 s1.intern()
和 s2.intern() ⽅法取得同⼀个字符串引⽤。intern() ⾸先把 “aaa” 放到 String Pool 中,然后返回这个字
符串引⽤,因此 s3 和 s4 引⽤的是同⼀个字符串。
在这里插入图片描述
在 Java 7 之前,String Pool 被放在运⾏时常量池中,它属于永久代。⽽在 Java 7,String Pool 被移到
堆中。这是因为永久代的空间有限,在⼤量使⽤字符串的场景下会导致 OutOfMemoryError 错误。

new String(“abc”)

  • 使⽤这种⽅式⼀共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。
    “abc” 属于字符串字⾯量,因此编译时期会在 String Pool 中创建⼀个字符串对象,指向这个 “abc”
    字符串字⾯量;
  • ⽽使⽤ new 的⽅式会在堆中创建⼀个字符串对象。

Object

public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {
    
    }
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException

构造器Constructor是否可被override

构造器不能被重写,不能用static修饰构造器,只能用public,private protected这三个权限修饰符,且不能有返回语句。

访问控制符public,protected,private,以及默认的区别

  • private只有在本类中才能访问;
  • public在任何地方都能访问;
  • protected在同包内的类及包外的子类能访问;
  • 默认不写在同包内能访问。

什么是泛型、为什么要使用以及泛型擦除

泛型,即“参数化类型”。
创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
类型擦除的主要过程如下:
1)将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2)移除所有的类型参数。

OKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK,今天结束。

猜你喜欢

转载自blog.csdn.net/hyfsbxg/article/details/122837081