1、String、StringBuffer、StringBuilder 的区别
String、StringBUffer、StringBffer都是java中的处理字符串的类。
String内部的字符数组都会用 final 关键字进行修饰,每当对 String 对象进行修改的时候,都会在内存中新开辟出空间创建出新的对象,旧的 String 对象就会频繁的被 JVM 回收,频繁触发垃圾回收,容易引起内存抖动现象。StringBuffer 和 StringBuilder 都允许对象被修改,在拼接、删除等操作过程中,通常不会像 String 那样每次产生新对象,只在扩容和最终 toString() 时创建新对象。所以如果需要频繁的拼接,删除字符串,StringBuffer 和 StringBuilder 的性能要高于 String ,因为前者不需要频繁的创建新对象。
String由于是不可变的,所以是线程安全的。StringBuffer中的方法使用 synchronized 关键字修饰,所以也是线程安全的。而StringBuilder是线程不安全的。
String适合在不需要经常变换的场景中使用,例如常量的声明、少量的变量运算。StringBuffer 适合运行在多线程环境中,因为 StringBuffer 在方法上加了 synchronized 锁。例如 XML 解析、HTTP 参数解析和封装。StringBuilder 适合用在单线程的环境中,如SQL语句的拼装、JSON封装等。
2、接口和抽象类的区别
接口是一个抽象的类型,它定义了静态常量,不能包含成员变量。接口中的方法默认是抽象的。接口中可包含抽象方法、默认方法和静态方法。接口的抽象方法是没有具体的实现的,但是默认方法和静态方法是可以有具体的实现的。
抽象类是一个类,可以包含抽象方法和具体方法,可以包含常量和变量。抽象类中的抽象方法是没有具体实现的方法,而具体方法是有具体实现的方法。抽象类不能实例化,需要通过子类继承来实现类中的抽象方法。
接口支持多继承,一个类可以继承多个接口。但是一个类只能继承一个抽象类。
接口不能包含构造器,因为接口不能被实例化,只能被继承。一个类继承一个接口,就必须实现接口中的所有的抽象方法。抽象类可以包含构造器,用来初始化子类实例,一个类继承了抽象类,那就必须实现抽象类中的全部的抽象方法。
接口中的方法默认是 public astract 的,接口中的变量默认是 public static final 的。
3、Java 常见的异常类有哪些
java中的异常都是继承自 Throwable 类。异常又分为 RuntimeException 和 其他异常。
运行时异常 RuntimeException 是程序运行的时候才可能出现的异常,比如数组越界 ArrayIndexOutOfBoundsException、空指针异常 NullPointException、类型转换错误 ClassCastException、除0异常等等。这些异常都是由于程序的逻辑有问题,是由程序自身的问题导致的。
其他异常包括除了运行时异常之外的异常,通常也被叫做编译时异常。通常是因为外部环境导致的,比如打开一个不存在的文件。编译器要求 java 程序必须捕获或声明所有的编译时异常,强制为可能出现的异常做保障。
4、说一说 Java 面向对象三大特性
java 面向对象的三大特征分别是封装、继承和多态。
封装是把一个对象的属性和行为封装到一起,对外只暴漏一些可用的接口。通常用 public、protected 和 private 进行修饰定义访问权限。
继承是指一个子类可以继承父类的属性和方法的机制。子类可以添加新的属性和方法,也可以重写父类的方法。提高了可重用性和可扩展性。java 只支持单继承,一个子类只能继承一个父类。
多态有两种形式,静态多态(编译时多态)和动态多态(运行时多态),编译时多态主要通过重载来实现。运行时多态是父类指针指向子类对象,运行时调用这个引用变量的方法的时候,方法的行为表现出子类方法的特征,就这会出现相同类型的变量调用同一个方法的时候,会产生不同的效果,通常通过重写来实现。
// 父类
class Animal {
void speak() {
System.out.println("动物发出声音");
}
}
// 子类
class Dog extends Animal {
@Override
void speak() {
System.out.println("汪汪汪");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("喵喵喵");
}
}
Animal a1 = new Dog(); // 父类引用 → 子类对象
Animal a2 = new Cat();
a1.speak(); // 输出:汪汪汪 ← 表现的是 Dog 的行为
a2.speak(); // 输出:喵喵喵 ← 表现的是 Cat 的行为
5、Java 重写和重载的区别
方法的重载是编译时多态,是指在同一个类中,可以有多个方法具有相同的名称,但是他们的参数列表互不相同。可以有不同的返回值类型,但是只有返回值类型不同的两个方法不构成重载。编译器在编译时根据参数的类型决定调用哪一个。
方法的重写是运行时多态,是指在子类中重新定义父类中的方法,方法名、参数列表和返回值都必须和父类中的相同。程序运行时根据对象的类型决定调用哪一个。运行时调用这个引用变量的方法的时候,方法的行为表现出子类方法的特征,就这会出现相同类型的变量调用同一个方法的时候,会产生不同的效果
6、final 关键字有什么作用
final 关键字就是不可变的意思,可以用来修饰属性、方法和类。
final 修饰类表示这个类是最终类,不可以被继承。
final 修饰方法表示该方法不能在子类中被重写,也就是说方法的行为是不变的。
final 修饰基本数据类型的变量表示该变量在一次初始化之后的数值就不能被更改。修饰引用类型的变量表示在这个引用类型的变量初始化之后就不能指向另一个对象,但是可以修改它指向的引用类型对象的内容。
final List<String> list = new ArrayList<>();
list.add("hello"); // 可以,修改对象内容
list = new ArrayList<>(); // 编译错误,不能改指向
7、== 和 equals 的区别
== 对于基本数据类型,比较的是两者的值是否相等;对于引用数据类型,比较的是两者指向的是不是内存中的同一个位置,也就是是不是同一个对象。
equal 是一个方法,定义在 Object 类中。在默认的情况下,比较的也是对象的引用,但是在子类中通常会被重写,比如在 Integer,String 中都已经完成重写,用来比较对象的内容是否相等。在重写 equals 方法的时候,需要重写 hashCode 方法。
8、Java的集合类有哪些
java 中的接口主要由 Collection 和 Map 两个接口派生。其中 Collection 接口又派生出三个接口,分别是 Set、List 和 Queue。
List接口:有序集合,允许重复元素。常见的实现类有 LinkedList 和 ArrayList。
Set接口:不允许有重复的元素。常见的实现类有 HashSet,LinkedHashSet,TreeSet
Queue接口:表示队列的数据结构。常见的实现类有 LinkedList,PriorityQueue。
Map接口:表示键值对的集合。常见的实现类有 HashMap,LinkedHashMap,TreeMap。
LinkedList、ArrayList、HashSet、HashMap 这些集合是线程不安全的集合。在多线程的环境中,需要适当的同步措施。TreeSet 和 TreeMap 也是线程不安全的集合。
vector 类似于 ArrayList,但是 vector是线程安全的,其中的每个公开的方法都用了synchronized 关键字进行修饰。因此在性能方面不如 ArrayList,更推荐使用 ArrayList。
HashTable 类似于 HashMap,但是是线程安全的。其中的每个公开的方法都用了synchronized 关键字进行修饰。是锁住整张表,因此性能很低。
ConcurrentHashmap 提供更好的并发性能,在 jdk1.7 之前通过锁分离技术实现线程安全,jdk1.8 已经实现了 CAS + synchronized 锁单个桶节点。
9、ArrayList 和 Array 有什么区别?ArrayList 和 LinkedList 的区别是什么
ArrayList 和 Array的区别
ArrayList 是动态数组实现的,Array 是固定长度的数组实现的;
ArrayList 提供了多种功能,比如自动扩容机制、增加和删除元素等,Array 没有这些功能;
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象,对于基本数据类型,需要使用包装类。
ArrayList 和 LinkedList 的区别
ArrayList 基于动态数组,LinkedList 基于双向链表。ArrayList 在随机访问的时候性能较好,但是 LinkedList 在随机访问的时候需要遍历整个链表,时间复杂度为 O(n);
在添加元素的时候,ArrayList 通常在末尾添加元素的时候很快,但是在中间添加元素的时候需要移动其后面的元素,时间复杂度为 O(n),但是 LinkedList 在添加元素的时候的时间复杂度为 O(1);
ArrayList 有扩容机制,当容量不足的时候,ArrayList 会进行扩容,会涉及到创建新数组和复置旧数组。 当需要频繁的随机访问某一个值的时候,可以使用 ArrayList;当经常需要进行删除和插入的操作的时候,更适合使用 LinkedList。
10、ArrayList 扩容机制
当创建一个 ArrayList 的时候,会分配一个初始容量,默认为10。当 ArrayList 中的容量达到了10之后。ArrayList 扩容的计算是在 grow() 方法里。grow()方法尝试先将数组的大小扩大为1.5倍。如果新的数组的容量满足需求,会调用一个 Arrays.copyof() 方法,将所有的元素从旧的数组中移到新的数组中,这个方法是真正的实现扩容的方法。
11、HashMap 的底层实现是什么,其读和写的时间复杂度是多少
在 jdk1.8之前,HashMap 是由数组和链表组成的,当发生哈希冲突的时候,多的元素会以链表的形式挂在数组的同一个位置。在 jdk1.8 之后引入了红黑树,当链表长度超过一定的阈值(默认为8)的时候,链表会转换成红黑树。红黑树在实现的时候不要求绝对的平衡,可以允许局部的不平衡,相较于 AVL 树来说减少了性能的开销。
最佳情况:时间复杂度为 O(1),元素分布均匀的情况下,通过 key 的哈希值直接定位到数组中的某一个位置,没有发生哈希冲突。
最坏情况:链表实现的时间复杂度为 O(n),红黑树实现的时间复杂度为 O(logn)
12、解决Hash冲突的方法有哪些,HashMap 是如何解决 hash 冲突的
解决哈希冲突的方法有链地址法和开放寻址法。
链地址法:当发生哈希冲突的时候,在数组的每个元素后面维护一个链表,把新来的元素挂在链表的尾部。JAVA 中的 HashMap 使用链地址法来解决哈希冲突。
开放寻址法:当发生哈希冲突的时候,采用某种参测方法在数组中寻找到空闲的位置来存储元素。常见的探测方法有线性探测、二次探测和双重哈希法。
13、HashMap 的 put 方法流程
步骤一:判断整个数组是不是空的,如果是的话,需要进行数组扩容。
步骤二:通过 key 算出应该插入的数组的索引,如果索引位置没有元素,那么直接把元素插入到数组,然后转到步骤六,检查是否数组满了。如果索引位置有元素,转到步骤三。
步骤三:判断数组相同索引位置已经存在的元素的 key 是否与该即将被插入的元素的 key 相同,如果相同直接覆盖原 value,否则转到步骤四。
步骤四:判断节点位置是不是一棵红黑树节点(TreeNode),如果是,就直接在红黑树中插入键值对,然后转到步骤六;如果不是红黑树节点,转到步骤五。
步骤五:遍历数组该位置的链表,如果已经发现这个相同的 key,直接覆盖,遍历到最后没有发现相同 key,直接插入到链表的尾部。然后判断链表的长度是否大于等于 8,如果大于 8,将链表转为红黑树(treeifyBin()),否则继续保持链表的形状。
步骤六:插入成功后,判断已经插入的数量是否大于最大容量阈值,超过的情况下需要进行扩容操作。
14、HashMap 的扩容机制
jdk1.7 版本,首先生成新数组。遍历数组中的每个位置上的链表的每个元素,获取每个元素的 key 值,并且基于新的数组的长度,算出每一个 key 在新的数组中的位置,将元素添加的新的数组中去,最后将新的数组赋值给原 HashMap 对象的 table 属性。
jdk1.8 版本,遍历整个数组中的每一个位置上的链表或者红黑树。如果是链表,就遍历链表上的全部的元素,然后重新计算 key 值在新的数组的位置,并添加到数组中去。如果是红黑树,先计算出每一个元素在新的数组中的位置,统计每个数组位置下标上的元素个数。如果元素个数小于 或等于 8,就生成一个链表,然后把链表的头节点放到数组该位置的元素中去。如果元素个数超过了 8,那就生成红黑树,并将根节点添加到数组中的对应位置上去。
15、HashMap 为什么是线程不安全的,如何实现线程安全
主要是因为他的操作不是原子性的,当两个线程同时执行读写操作时,一个线程进行写操作,另一个线程进行读操作,可能导致数据不一致的情况。当在多线程的情况下,检查再写入这种操作存在竞争条件,可能被其他线程插入,导致重复 put 或数据覆盖。
if (!map.containsKey(k)) map.put(k, v);
可以使用 Collections.synchronizedMap() 方法,该方法创建出来一个线程安全的 HashMap,通过给每个方法加 synchronized 来实现单次操作的线程安全,该方法返回一个同步的 Map 包装器,使得对所有的 Map 操作都是同步的。
HashMap<String, Integer> raw = new HashMap<>();
Map<String, Integer> safe = Collections.synchronizedMap(raw);
可以使用 ConcurrentHashMap,专门为了多线程环境下设计的哈希表。
16、ConcurrentHashMap 如何保证线程安全
首先看 jdk1.7 版本之前的场景,主要是使用分段锁的设计来实现的。首先将整个 map 分成若干个Segment,每个 segment 继承了 ReentrantLock 并内部维护一个 hashentry,这个 hashentry 就是真正用来存储键值对的地方,每一个 segment 共享同一把锁。在写入的时候,需要先通过每个 key 的 hash 值来定位到 segment 的具体位置,然后只对这个 segment 加锁(lock()),操作完了之后再解锁(unlock())。在读的时候不加锁,因为 hashentry 的 value 和 next 字段默认添加了 volatile 字段,保证了内存的可见性。
再以 jdk1.8 为例,主要是由数组、链表和红黑树组成的,底层使用 CAS 和 synchronized 锁机制来实现并发安全。写入的时候,如果这个数组元素没有值,那么直接用 CAS 乐观锁来实现对数组的写入操作;如果有值,那就需要通过 synchronized 锁进行同步操作,锁的粒度是槽位。读取的时候,主要因为 Node 节点中的 val 和 next 字段前面添加了 volatile 实现了可见性,一个线程写入后,其他的线程立刻就可以看到被写入的数据,提高了性能。
17、HashMap 和 HashTable 的区别
HashTable 是同步的,是通过在每个方法上面添加 synchronized 关键字来实现的,可以再多线程的环境下使用,保证并发的安全性。但是 HashMap 不是线程安全的,但是可以通过 Collections.synchronizedMap() 来创建一个线程安全的 HashMap。
由于 HashTable 是同步的,所以他的性能比较差。相对而言由于 HashMap 是异步的,所以他的性能就比较好,免去了一些枷锁和解锁的操作。
HashTable 不允许键或值为空值,但是 HashMap 的键和值都可以为空值。
18、HashMap 和 HashSet 的区别
18、Java 创建线程有哪几种方式
继承 Thread 类:通过创建 Thread 类的子类,然后重写其中的 run 方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
new MyThread().start();
实现 Runnable 接口:创建一个实现了 Runnable 接口的类,并实现其中的 run 方法。然后创建该类的实例,将实例以参数的形式传递给 Thread 对象。
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务运行中: " + Thread.currentThread().getName());
}
}
new Thread(new MyTask()).start();
// 等价于
Runnable task = new MyTask();
Thread t = new Thread(task);
t.start();
使用 Callable 和 Future 接口:创建一个 Callable 的接口的实现类,并实现其中的 call 方法,该方法可以返回结果和抛出异常。通常使用 FutureTask 对象来包装 Callable,然后将 FutureTask 当作参数传递到 Thread 对象。
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞获取返回值
使用线程池创建线程:通过使用 Excutors 类进行创建线程池,通过线程池来管理线程。
ExecutorService pool = Executors.newFixedThreadPool(4);
// 提交没有返回值的任务
pool.submit(new PrintTask());
// 提交有返回值的任务
Future<Integer> future = pool.submit(new ComputeTask());
Integer value = future.get();
pool.shutdown();
19、线程start和run的区别
run 方法是线程的执行体,包括线程要执行的代码。如果直接调用 run 方法,那么会直接在本线程內部的上下文中执行,并不会创建一个新的线程。
start 方法用来创建一个新的线程,run 方法中的内容会在这个线程中执行。当开始调用的时候,操作系统会为线程分配系统资源,并将线程的状态设置为就绪态,下次 cpu 调度的时候就可以开始执行了。
20、你知道Java中有哪些锁吗
公平锁/非公平锁:公平锁是指多个进程按照申请锁的顺序获取锁,非公平锁的锁的获取顺序和申请锁的顺序并不完全一致。可能会导致有些进程发生饥饿。ReentrantLock 默认就是非公平锁,synchronized 也是一种非公平锁。
可重入锁:是指在同一个线程的外层方法获取到了锁之后,内层方法也可以获取到锁。ReentrantLock 和 Synchronized 都是可重入锁。
独享锁/共享锁:独享锁是指在同一时间只能有一个线程持有,共享锁是指该锁可以被多个线程持有。ReentrantLock 和 Synchronized 都是独享锁。Lock 的实现类中 ReadwriteLock 是读的时候共享锁,写的时候是独占锁。
乐观锁/悲观锁:这两种锁看待并发的角度不同。悲观锁会认为在并发的场景下,对同一个资源的访问一定会发生修改,所以在对这个资源进行修改之前,一定要上锁。乐观锁会认为在访问的时候,一般不会发生修改,所以只有在想要修改的时候,检查是否可以更改,如果不可以的话就重试。在对资源的写操作多的时候,更适合使用悲观锁。如果是读操作更多的场景下,使用乐观锁效率更高,因为没有上锁和解锁的资源开销。
偏向锁/轻量级锁/重量级锁:是针对 Synchronized 锁的三种锁的状态。
自旋锁:在尝试获取锁失败的情况下不会阻塞线程,而是一直循环等待取获取,减少上下文切换带来的开销,但是也会消耗 CPU。
21、对 synchronized 的理解
当一个方法或者代码块被 synchronized 修饰的时候,这部分临界区在同一时间只能有一个线程进行访问,其他线程必须等待当前线程释放锁才能访问资源。
当 synchronized 修饰实例方法的时候,锁住的是这个对象,不同的实例之间互相不影响。修饰静态方法的时候,锁的是 Class 对象,该对象的所有的实例共享同一把锁。修饰代码块的时候,锁对象是由我们自己指定的,可以选择性的锁住代码的一部分,而不是整个方法,提高效率。随着 jdk 的不断更新,synchronized 方法也从原来的必须要切换回内核态,升级到了现在的偏向锁、轻量级锁和重量级锁。减少了资源的消耗。锁的状态记录在对象头的一个字段中。偏向锁是指同一个线程一直访问一段同步代码,该线程自动获取锁。当处于偏向锁的时候,突然被另一个线程访问,偏向锁会升级成轻量级锁,其他的线程会一直处于自旋等待的状态。当自旋超过了一定的阈值,或者又有其他的线程到达的时候,轻量级锁就会升级成重量级锁。这个时候没有获取锁的线程不会再自旋等待,而是进入阻塞态,由操作系统负责调度。也就意味着线程由用户态进入了核心态,开销较大。
22、synchronized 和 ReentrantLock 的区别是什么
都是用来实现线程同步的手段,synchronized 是基于 JVM 的内置锁实现的,可以修饰方法和代码块,使用起来比较简单直接。而 ReentrantLock 是 locks 包下的一个锁的实现,需要显示创建,并且需要手动的 lock() 和 unlock() 进行锁的获取和释放。
synchronized 灵活性相对较低,他只能用来修饰方法和代码块。方法一旦开始执行,在等待线程获取锁的时候,也不能中断,没有超时机制,一旦获取不到锁就会一直等待,线程调度由 JVM 控制。synchronized 可以通过 wait()、notify() 和 notifyAll() 与对象的监视器配合使用。
private final Object lock = new Object(); // 锁对象
private int stock = 0; // 库存,容量 1
// 生产者
synchronized (lock) {
while (stock >= 1) lock.wait(); // 满了 → 等待(对应 notFull.await())
stock++; // 放入元素
lock.notifyAll(); // 唤醒消费者(对应 notEmpty.signal())
}
// 消费者
synchronized (lock) {
while (stock <= 0) lock.wait(); // 空了 → 等待(对应 notEmpty.await())
stock–; // 取出元素
lock.notifyAll(); // 唤醒生产者(对应 notFull.signal())
}
ReentrantLock 支持中断操作,可以在等待锁的过程中进行中断响应。支持获取所的过程中设置超时时间,可以设置成公平锁,可以检查锁的状态。ReentrantLock 可以通过 Condition 实现更灵活的条件变量控制:一个 Lock 可创建多个 Condition,每个 Condition 有独立等待队列,可按条件精确唤醒,避免无效唤醒。
ReentrantLock lock = new ReentrantLock(); // 仓库大门的唯一钥匙
Condition notFull = lock.newCondition(); // "货架不满"等候室(生产者等这)
Condition notEmpty = lock.newCondition(); // "货架有货"等候室(消费者等这)
lock.lock(); // ① 拿钥匙进门
try {
while (货架满) { // ② 看看货架满没满
notFull.await(); // ③ 满了 → 去"不满"等候室等,同时把钥匙交出去
}
放入元素; // ④ 不满 → 放一个货
notEmpty.signal(); // ⑤ 喊醒"有货"等候室里的一个消费者
} finally {
lock.unlock(); // ⑥ 出门,交出钥匙
}
lock.lock(); // ① 拿钥匙进门
try {
while (货架空) { // ② 看看货架空没空
notEmpty.await(); // ③ 空了 → 去"有货"等候室等,交出钥匙
}
取出元素; // ④ 有货 → 取一个
notFull.signal(); // ⑤ 喊醒"不满"等候室里的一个生产者
} finally {
lock.unlock(); // ⑥ 出门交钥匙
}
synchronized 和 ReentrantLock 都能实现生产者-消费者,逻辑完全一样,核心区别在于等待队列的数量:synchronized 只有一个等待队列,生产者和消费者都睡在一起,没法区分该唤醒谁,所以只能用 notifyAll() 把所有人都喊醒,被误唤醒的线程发现条件不满足又得睡回去,产生无效唤醒;而 ReentrantLock 可以创建多个 Condition,给生产者、消费者各建一个独立队列,放货只 signal 消费者、取货只 signal 生产者,精准唤醒该醒的线程,效率更高。
23、volatile 关键字的作用有那些?
volatile 关键字通常被叫做轻量级的 synchronized,不需要获取锁和释放锁,但是只能修饰变量,不能用于方法和代码块。可以保证变量的内存可见性和防止指令重排。
保证可见性:当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝一份到工作内存中,每个线程内对该变量的修改在被同步到主内存之前,其他的线程是不可见的。被 volatile 修饰之后,底层是通过汇编 lock 前缀指令,会将当前处理器缓存行立即写回到主内存中,然后该操作会使得其他 CPU 缓存中缓存了该内存地址的数据无效,然后其他 CPU 再去主内存中及时的获取最新值(缓存一致性协议)。
禁止指令重排:指令重排序是 JVM 提升程序运行效率,在不影响单线程程序的执行结果(as-if-serial)以及 JMM 的跨线程语义不变约束(happens-before),提高并行度的方法。volatile 指令会禁止指令重排序,使用汇编 lock 前缀指令,在硬件级别已经实现好了,在读写操作指令的前后会插入内存屏障,使其和前后的代码不能进行重排序。
24、volatile 与synchronized 的对比
synchronized 锁用来提供线程的同步机制,当一个线程进入一个由 synchronized 锁修饰的代码块或方法的时候,这个线程会获取到这个锁对象的监视器锁(monitor),确保同一时间只有一个线程可以执行这段代码或者方法,实现了一致性和安全性。
volatile 用来修饰变量,可以保证变量的有序性和可见性。当一个线程对变量进行了修改,其余的线程可以立刻知道,然后获取到主内存中变量的最新值。volatile 可以防止 JVM 对被修饰指令的重排序。但是,volatile 不能保证原子性。
synchronized 关键字可以实现对修饰的代码块中的代码的原子性,保证其在执行期间不被其他线程打断。volatile 关键字只能保证修饰的变量在不同线程之间的可见性和有序性,对于复合操作(自增,自减)等不能保证原子性。
volatile 要比 synchronized 的性能要好,因为不涉及底层的对锁的获取和释放的操作,但是他的同步的级别也比较低。
25、JDK8有哪些新特性
lambda 表达式:允许以更简洁的方式编写匿名函数。
新的日期 API:引入了 java.time 包,提供了全新的日期API,比如 LocalDate,LocalDateTime,解决了之前出现的问题。
Stream API:提供了一种声明式的方式来对集合进行处理的操作。比如过滤、映射等。
26、为什么要有线程池
在多线程的任务中,每个线程都要占用内存和 CPU 资源,如果不加以限制,任由线程无限的创建下去,会导致内存资源的不足。通过线程池可以避免这个问题。
如果有些线程的存活时间短,但是又不得不创建这个线程,就可交给线程池来管理,避免了重复的线程的创建和销毁造成的资源的消耗。线程池可以创建并管理一部分线程,当有任务到来的时候,先放到线程池的任务队列中,由线程池决定用哪个空闲的线程去执行这个任务,当线程不够的时候,创建一部分线程来执行任务,但是这个数量收到最大线程数的限制;当线程冗余的时候,就是超出了空闲存活时间,销毁一部分线程。
27、说一说线程池有哪些常用参数
corePoolSize 核心线程数:线程池中长期存活的线程数
maximumPoolSize 最大线程数:线程池中允许创建的最大的线程的数量,当线程池的任务队列满了的数量,还能创建多少线程。最大线程数 = 核心线程数 + 队列满之后还能额外创建的非核心线程数。
keepAliveTime 空闲线程存户时间:当线程数大于 corePoolSize 时,空闲线程最大的等待新任务的存活时间。
timeUnit:与 keepAliveTime 一起使用,指定 keepAliveTime 的时间单位,如秒、分钟等。
workQueue 线程池工作队列:线程池存放任务的队列,用来存储所有的线程池的正在等待的任务。
ThreadFactory 创建线程的工厂:线程池创建线程的时候调用的工厂函数,可以设置线程的优先级、线程命名规则和线程类型(守护/非守护)等。
RejectExecutionHandler 拒绝策略:当线程池的任务超过了最大线程数的时候,执行拒绝的策略。
28、BIO、NIO、AIO 的区别
BIO、NIO、AIO 是 java 中不同的I/O模式,在处理输入输出操作是对应不同的动作。
BIO:阻塞式I/O模型,当线程正在执行的时候,需要进行I/O操作,但是数据还没有准备好,线程会进入阻塞等待状态,CPU 转去执行其他的线程,数据准备好之后,硬件设备会发生中断,CPU会响应该中断,操作系统进入内核态,执行中断处理程序,内核处理后会唤醒该线程,CPU再来继续执行这个线程。
NIO:非阻塞式I/O模型,使用了缓冲区和通道来进行数据处理,提高了I/O操作的效率。
AIO:异步I/O模型,线程发起I/O操作,不等待,直接返回。操作系统在后台异步的执行I/O操作,操作执行结束之后,再通过回调函数通知线程I/O操作已完成。
BIO 适合低并发、连接少的场景。NIO 适合高并发、连接量大的场景。AIO 适合性能需求比较高的场景。
29、JVM 方面先跳过
30、对 Spring 中 IOC 的理解
spring 控制反转,核心的思想是让对象的创建和依赖关系由容器来管理,不是我们自己 new 出来的,这样方便管理也简化了开发。
这个容器其实就是一个 Map,里面封装好了各种对象。通过 DI 依赖注入,spring 容器可以动态的将这些依赖注入到需要他们的对象当中。而不需要对象自己去创建或寻找依赖。极大程度上简化了应用的开发。
当前习惯用 springboot 注解配置的方式来管理依赖。使用配置可以告诉 spring 容器如何创建,如何和管理对象的生命周期。
31、对 Spring 中 AOP 的了解
面向切面编程利用一种“横切”的技术,将那些影响了多个类的公共行为封装到一个对象中,并把这个对象叫做“切面”,将那些与具体业务无关的信息抽取出来,统一管理。可以减少系统中的重复代码,降低模块之间的耦合度,增加了系统的可操作性和可维护性。
spring 中的 AOP动态代理由 IOC容器负责创建和管理。AOP动态代理可以直接使用目标对象和切面对象构造动态代理。spring 对象默认使用JDK动态代理来创建动态代理,这样可以为任意一个接口对象实例创建动态代理。
具体实现 AOP 的步骤是:首先定义目标对象,然后定义切入点,一个切入点可以横切多个目标对象中的多个方法。最后定义增强处理,把这些逻辑织入到目标方法中,决定这段代码在目标方法执行的前、后、或者环绕时运行。
32、Bean 的作用域
在 spring 中,那些由 spring 管理的对象称为 bean,bean 的作用域决定了这个 bean 在整个应用程序中的生命周期和可见范围。
单例模式:默认的作用域,当一个 bean 的作用域为 singleton 的时候,整个 IOC 容器中只有一个 bean 的实例,所有对 bean 的请求,只要 id 与该 bean 的定义相匹配,都是使用这一个实例。
原型模式:当一个 bean 的作用域为 prototype 的时候,是原型实例,表示一个 bean 定义对应的实例可以有多个对象实例。每次对该 bean 的请求都会返回不同的实例。
请求模式:一个 HTTP 请求对应一个不同的 bean 实例,这个 bean 只在请求期间好用。
会话模式:一个 HTTP 会话对应一个 bean 的实例,生命周期和会话的生命周期相同。
33、Bean 的生命周期
spring bean 的生命周期就是一个 bean 从创建到销毁的完整的过程。
1、实例化 bean:调用默认构造方法或者工厂方法创建实例化 bean。
2、依赖注入:容器注入 bean 的属性,这些属性可能是其他 bean 的引用,也可能是普通类型。
3、检查 Aware 接口并设置相关的依赖:如果一个 bean 实现了 BeanNameAware 或者 BeanFactoryAware 接口,容器会调用相关的方法。
4、BeanPostProsscer:在Bean的属性设置之后,Spring会调用所有实现 BeanPostProcessor 接口的 postProcessorBeforeInitialization() 方法。
5、如果Bean实现了 InitialingBean 接口,容器会调用其afterPropertiesSet方法。
6、初始化Bean:如果Bean定义了 init-method,容器也会调用这个方法。
7、BeanPostProcessor的第二次调用:容器会再次调用所有注册的 BeanPostProcessor 的postProcessorAfterInitialization() 方法,这次是在Bean初始化之后。
8、使用Bean:此时,Bean已经准备好了,可以被应用程序使用了。
9、处理 DisposableBean :当容器关闭时,如果Bean实现了DisposableBean接口,容器会调用其destroy方法。
10、Bean销毁:最后,Bean被Spring容器销毁,结束了它的生命周期。如果Bean定义了destroy-method,容器也会调用这个方法。
34、Spring循环依赖是怎么解决的
当两个或两个以上的 bean 互相持有对方的依赖的时候,比如 bean A 依赖于 bean B,同时 bean B 也依赖于 bean A,就会导致循环依赖,可能导致容器创建对象失败。
可以给 bean 打标签用来判断是否发生了循环依赖,比如 bean A 在创建的时候,依赖于 bean B,然后去创建 B,在创建 B 的时候,发现又需要创建 A,于是去创建 A,但是发现 A 此时正在被标记创建中,说明发生了循环依赖。
可以使用三级缓存来解决循环依赖问题。首先,当一个 bean 对象实例化好了之后,spring 会注册一个 singletonFactory 到三级缓存(不同的 bean 对应各自的自己的 bean 工厂),一旦有其他的 bean 在需要当前 bean 的依赖的时候,可以制造还没有初始化的 bean(不完善的代理对象)。
当为该 bean 填充属性的时候,如果依赖其他的bean,而那个 bean 又依赖当前的 bean,spring 就会从尝试从缓存中获取当前 bean,优先从一级缓存中获取,发现没有,就代表 A 还没有成品可用,于是转去二级缓存中获取,发现也没有,于是去三级缓存中找到缓存工厂,生成早期的引用放入二级缓存,供其他的 bean 注入。
等到该 bean 的属性填充完成,初始化完毕之后,spring 会把这个 bean 创建代理对象放到一级缓存中,同时清理二级缓存和三级缓存中的早期引用。
网硕互联帮助中心






评论前必须登录!
注册