SE基础篇

1.java 面向对象的特征

|

封装 | 把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。 | | | | —- | —- | —- | —- | | 继承 | 子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性 | | | |

多态 | 指允许不同的对象对同一消息做出相应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。 | | |

2.请你说说面向对象的六大原则?

单一职责原则 对一个类而言,应该仅有一个引起它变化的原因
开放封闭原则 软件实体应该是可以扩展的,但是不可修改
Liskov替换原则 子类型必须能够替换掉它们的基类型
依赖倒置原则 抽象不应依赖于细节,细节应该依赖于抽象
接口隔离原则 多个专用接口优于一个单一的通用接口
良性依赖原则 不会在实际中造成危害的依赖关系,都是良性依赖

3.谈谈final、finally、finalize的区别

final finally finalize
java关键字 java关键字 java的一个方法名
final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
在异常处理中,try子句中执行需要运行的内容,catch子句用于捕获异常,finally子句表示不管是否发生异常,都会执行。finally可有可无。但是try…catch必须成对出现 在垃圾收集器删除对象之前对这个对象调用的子类覆盖 finalize() 方法以整理系统资源或者执行其他清理操作。

4.请你说说java内部类和静态内部类的区别

内部类 静态内部类
非静态内部类则不能有静态成员(方法,属性) 静态内部类可以有静态成员(方法,属性)
非静态内部类则可以访问外部类的所有成员(方法,属性) 静态内部类只能够访问外部类的静态成员
OutClassTest oc1 = new OutClassTest()
OutClassTest.InnerClass nostaticinner = oc1.new InnerClass();
不依赖于外部类的实例,OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();

5.请你简单讲解一下java泛型。

泛型: 即参数化类型。本质是在不创建新的类型的情况下,通过泛型指定不同的类型来控制形参具体限制的类型。
泛型的特点: 泛型只在编译阶段有效,泛型信息不会进入到运行时阶段。
泛型的三种使用方式: 泛型类、泛型接口、泛型方法。

泛型通配符: | 符号,可以解决当具体类型不确定的时候,这个通配符就是 ?;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。注意:?是类型的实参,而不是类型形参。 | | | 泛型上下边界: | 上边界,即传入的类型实参必须是指定类型的子类型。
下边界,即传入的类型实参必须是指定类型的基类型。 | | |

泛型方法: | 只有在public和返回值之间声明了“”的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。 | | | 泛型的数组: | 官方文档有说明,不能创建一个明确的泛型类型的数组。但是使用通配符创建泛型数组是可以的。 | |

6.请讲一下java中的==,equals与hashCode的区别与联系

== 该操作符生成的是一个boolean结果,它计算的是操作数的值之间的关系
equals Object 的 实例方法,比较两个对象的content是否相同,在String、Double等类中已经重写了equals方法,所以有了另外一种计算公式,是进行内容的比较。
hashCode Object 的 native方法 , 获取对象的哈希值,用于确定该对象在哈希表中的索引位置,它实际上是一个int型整数

hashCode和equals的关系: | 如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同
如果两个对象的hashCode相同,它们的对象并不一定相同(即用equals比较返回false) | |

7.请你讲一下int与integer的区别

Integer与int比对
Integer是int的包装类 int是基本数据类型
Integer变量必须实例化后才能使用 int变量不需要
Integer实际是对象的引用,指向此new 的Integer对象 int直接存储数据值
Integer的默认值是null int的默认值是0
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)
Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
java的两种数据类型
基本数据类型 byte、short、int、long、float、double、char、boolean
引用数据类型 数组、类、接口、枚举
基本数据类型的封装类 Byte、Short、Int、Long、Float、Double、Char、Boolean


自动装箱/拆箱

自动装箱:将基本数据类型重新转化为对象

自动拆箱:将对象重新转化为基本数据类型

| | |


自动装箱/拆箱原理 | java对于Integer与int的自动装箱与拆箱的设计,是一种模式:叫享元模式(flyweight)。加大对简单数字的重利用,Java定义在自动装箱时对于值从–128到127之间的值,它们被装箱为Integer对象后,会存在内存中被重用,始终只存在一个对象。而如果超过了从–128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个 Integer对象。 | |

8.java中重载与重写的区别

| 重写

重载
参数列表必须完全与被重写的方法相同 必须具有不同的参数列表;
返回的类型必须一直与被重写的方法的返回类型相同 可以有不相同的返回类型,只要参数列表不同就可以了;
访问修饰符的限制一定要大于或等于被重写方法的访问修饰符 可以有不同的访问修饰符;
重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。 可以抛出不同的异常;

9.请简要谈谈java的反射机制

|

概念 | 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制 | | | —- | —- | —- | | 实例对象的三种表达方式 | 1、类名.class;2、对象名.getClass( );3.Class.forName(全限定类名) | | | class类的动态加载 | Class.forName(类的全称);该方法不仅表示了类的类型,还代表了动态加载类。编译时刻加载类是静态加载、运行时刻加载类是动态加载类。 | | |

获取方法信息 | getMethods( )获取所有public修饰的方法信息;
getDeclaredMethods( )获取所有方法信息包括private修饰的方法信息; | | |

获取成员变量信息 | getFileds( )获取所有public修饰的成员变量;
getDeclaredFileds( )获取所有成员变量信息包括private修饰的成员变量信息; | | | 获取构造函数信息 | getDeclaredConsttuctors()方法 | | | 方法的反射操作 | method.invoke(对象,参数列表); | |

10.String、StringBuffer、StringBuilder三者之间的区别(必会)

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
String 中的String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[ ] ,String对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

11.接口和抽象类的区别是什么?(必会)

实现: 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数: 抽象类可以有构造函数;接口不能有。
main 方法: 抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
实现数量: 类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符: 接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符

12.string常用的方法有哪些?(了解)

indexOf(): 返回指定字符的索引。
charAt(): 返回指定索引处的字符。
replace(): 字符串替换
trim(): 去除字符串两端空白。
split(): 分割字符串,返回一个分割后的字符串数组。
getBytes(): 返回字符串的 byte 类型数组。
length(): 返回字符串长度。
toLowerCase(): 将字符串转成小写字母。
toUpperCase(): 将字符串转成大写字母。
substring(): 截取字符串。
equals(): 字符串比较。

13. jdk1.8的新特性(高薪常问)

1)Lambda 表达式 :允许把函数作为一个方法的参数。
2)方法引用 :方法引用允许直接引用已有 Java 类或对象的方法或构造方法。比如:**System.out::println **
3)函数式接口:有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为 Lambda 表达式。通常函数式接口上会添加@FunctionalInterface 注解。
4)接口允许定义默认方法和静态方法 。
5)Stream API:这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。
6)日期/时间类改进:
之前的 JDK 自带的日期处理类非常不方便,日期时间的创建、比较、调整、格式化、时间间隔等。 这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。
7)Optional 类 :Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。
8) Java8 Base64 实现 :Java 8 内置了 Base64 编码的编码器和解码器。

14. Java的异常(必会)

Throwable: 是所有Java程序中错误处理的父类,有两种子类:Error和Exception。

Error: | 表示由JVM所侦测到的无法预期的错误,由于这是属于JVM层次的严重错误,导致JVM无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。 | | | Exception: | 表示可恢复的例外,这是可捕捉到的。 | | | 运行时异常: | 都是RuntimeException类及其子类异常 | | | 非运行时异常 (编译异常): | 是RuntimeException以外的异常 | | | 常见的RunTime异常: | 空指针引用异常 、类型强制转换异常、下标越界异常、算术运算异常等 | |

15.BIO、NIO、AIO 有什么区别?(高薪常问)

BIO Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

16.Threadlocal的原理(高薪常问)

ThreadLocal:为共享变量在每个线程中创建一个副本,每个线程都可以访问自己内部的副本变量。通过threadlocal保证线程的安全性。
其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本。
ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的。如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。
使用完ThreadLocal之后,记得调用remove方法。 在不使用线程池的前提下,即使不调用remove方法,线程的”变量副本”也会被gc回收,即不会造成内存泄漏的情况。

17.同步锁、死锁、乐观锁、悲观锁 (高薪常问)

|

同步锁 | 指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。 | | | —- | —- | —- | | 死锁 | 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。 | | |

乐观锁 | 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。适用于多读的应用类型。使用场景:数据库的write_conditio机制 | | |

悲观锁 | 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。使用场景:数据库的行锁,表锁等,读锁,写锁等。 | |

18.说一下 synchronized 底层实现原理?(高薪常问)

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
· 普通同步方法,锁是当前实例对象
· 静态同步方法,锁是当前类的class对象
· 同步方法块,锁是括号里面的对象

19.synchronized 和 volatile 的区别是什么?(高薪常问)

volatile synchronized
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取 synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别 synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性 synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞 synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化 synchronized标记的变量可以被编译器优化。

20. synchronized 和 Lock的区别是什么?(高薪常问)

synchronized Lock
synchronized是java内置关键字 在jvm层面,Lock是个java类
synchronized无法判断是否获取锁的状态 Lock可以判断是否获取到锁
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁) Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去 Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平 Lock锁可重入、可判断、可公平(两者皆可);
synchronized锁适合代码少量的同步问题 Lock锁适合大量同步的代码的同步问题

31.java中的设计模式-单例模式

1.单例模式
单例模式的五种实现方式?

  • 饿汉式单例

实现:构造方法私有,提供一个静态的且被final修饰的成员变量,变量的值为私有构造创建出来的唯一实例,提供一个静态的公共方法,方法名一般为**getInstance()**,方法实现返回前面的静态变量。
三种可以破坏饿汉式单例方式:

  1. 1. **反射破坏单例**

解决:在构造方法里做判断,如果成员变量 !=null 抛出异常

  1. if(instance != null){
  2. throw new RuntimeException(".............");
  3. }
  1. 2. **反序列化破坏单例**

解决:写一个**readResolve()**方法,在方法里返回成员变量

  1. public Object readResolve(){
  2. return instance;
  3. }
  1. 3. **Unsafe破坏单例**

无预防方案。
枚举饿汉式单例:
实现:

  1. 具体实现:
  2. public enum Singleton{
  3. INSTANCE;
  4. public static Singleton getInsatance(){
  5. return INSTANCE;
  6. }
  7. }
  8. 原理:
  9. enum Sex{
  10. MALE,FEMALE;
  11. }
  12. final class Sex extends Enum<Sex>{
  13. public static final Sex MALE;
  14. public static final Sex FEMALE;
  15. private Sex(String name,int ordinal){
  16. super(name,ordinal);
  17. }
  18. static{
  19. MALE = new Sex{"MALE",0};
  20. FEMALE = new Sex{"FEMALE",1}
  21. $VALUES = values();
  22. }
  23. }

注意:反射和反序列化都不能破坏枚举创建出来的单例模式,但是unsafe可以破坏。
懒汉式单例:
实现:

  1. public class Singleton implements Serializable{
  2. private Singleton(){
  3. return INSTANCE;
  4. }
  5. private static Singleton INSTANCE = null;
  6. public static synchronized Singleton getInstance(){
  7. if(INSTANCE == null){
  8. INSTANCE = new Singleton();
  9. }
  10. return INSTANCE;
  11. }
  12. }

注意:**getInstance()**方法必须加锁,否则在多线程环境下就会出现重复创建实例的现象,还有一个问题:只需要在第一次创建实例时,需要互斥保护,当对象创建完毕后,就不在需要锁了,目前的解决方案性能太低。
DCL(双检索)懒汉式单例:
实现:

  1. public class Singleton implements Serializable{
  2. private Singleton(){
  3. return INSTANCE;
  4. }
  5. private static volatile Singleton INSTANCE = null;
  6. public static Singleton getInstance(){
  7. if(INSTANCE == null){
  8. synchronized(Singleton.class){
  9. if(INSTANCE == null){
  10. INSTANCE = new Singleton();
  11. }
  12. }
  13. }
  14. return INSTANCE;
  15. }
  16. }

注意:**volatile**的作用是解决共享变量的可见性问题和有序性问题。在这里主要是为了保证共享变量的有序性问题。volatile给**INSTANCE = 对象**添加了一个内存屏障,阻止底层CPU指令的重排序,保证先执行构造方法,然后给对象赋值。因为底层CPU指令出于优化的目的,有可能会对指令进行重新排序,导致先给对象赋值再执行构造方法,但是在多线程环境下这样做是有问题的,比如线程A给对象赋值,但还没有执行构造方法,但是线程B已经拿着这个半成品对象走了,最终出现BUG。

懒汉式单例-内部类
实现:

  1. public class Singleton implements Serializable{
  2. private Singleton(){
  3. return INSTANCE;
  4. }
  5. private static class Holder{
  6. static Singleton INSTANCE = new Singleton{};
  7. }
  8. public static Singleton getInstance(){
  9. return Holder.INSTANCE;
  10. }
  11. }

静态代码块的线程安全问题是由JVM来保证的,即有static关键字修饰的成员变量和方法,不需要考虑线程安全。
jdk哪些地方体现了单例模式?

  1. Runtime的这个类是一个典型的饿汉式单例,

    1. public static Runtime getRuntime(){
    2. return currentRuntime;
    3. }
  2. System类是一个典型的DCL懒汉式单例: ```java private static volatile Console cons;

public static Console console(){ Console c; if((c == cons) == null){ synchronized(System.class){ if((c == cons) == null){ cons = c = SharedSecrets.getJavaIOAccess().console(); } }
} return c; }

  1. 3. **Collections集合类有很多懒汉式单例-内部类和饿汉式单例:empty开头的变量很多都是这种单例模式**
  2. ```java
  3. 懒汉单例-内部类:
  4. private static class EmptyNavigableSet<E> extends UnmodifiableNavigableSet<E>
  5. implements Serializable {
  6. private static final long serialVersionUID = -6291252904449939134L;
  7. public EmptyNavigableSet() {
  8. super(new TreeSet<E>());
  9. }
  10. private Object readResolve(){
  11. return EMPTY_NAVIGABLE_SET;
  12. }
  13. }
  14. private static final NavigableSet<?> EMPTY_NAVIGABLE_SET =
  15. new EmptyNavigableSet<>();
  16. 反向比较器:
  17. public static <T> Comparator<T> reverseOrder() {
  18. return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
  19. }
  20. private static class ReverseComparator
  21. implements Comparator<Comparable<Object>>, Serializable {
  22. private static final long serialVersionUID = 7207038068494060240L;
  23. static final ReverseComparator REVERSE_ORDER
  24. = new ReverseComparator();
  25. public int compare(Comparable<Object> c1, Comparable<Object> c2) {
  26. return c2.compareTo(c1);
  27. }
  28. private Object readResolve() { return Collections.reverseOrder(); }
  29. @Override
  30. public Comparator<Comparable<Object>> reversed() {
  31. return Comparator.naturalOrder();
  32. }
  33. }
  34. 饿汉单例:
  35. public static final Set EMPTY_SET = new EmptySet<>();
  36. public static final List EMPTY_LIST = new EmptyList<>();
  37. public static final Map EMPTY_MAP = new EmptyMap<>();
  1. Comparators枚举饿汉式单例:

    1. 枚举饿汉式单例:
    2. private Comparators() {
    3. throw new AssertionError("no instances");
    4. }
    5. enum NaturalOrderComparator implements Comparator<Comparable<Object>> {
    6. INSTANCE;
    7. @Override
    8. public int compare(Comparable<Object> c1, Comparable<Object> c2) {
    9. return c1.compareTo(c2);
    10. }
    11. @Override
    12. public Comparator<Comparable<Object>> reversed() {
    13. return Comparator.reverseOrder();
    14. }
    15. }

    多线程篇

    1.java的线程状态有哪些?

    线程的六种状态.png
    新建(NEW):线程对象创建的时刻;
    可运行(RUNNABLE):线程状态调用**start()**方法;
    终结(TERMINATED):线程把代码运行完毕时;
    阻塞(BLOCKED):线程争抢锁失败时,进入这个状态;当线程获取锁成功时,就会进入可运行状态;
    等待(WAITING):当线程获得锁之后,发现还需要一些条件才能继续执行,调用**wait()**方法进入该状态并且释放锁,当条件满足时调用**notify()**方法唤醒该线程重新争抢锁。注意:不调用notify方法,将一直不执行;
    有时限的等待(TIMED_WAITING):第一种,获得锁后线程对象调用**wait(long time)**方法并加入时间的参数,进入睡眠状态;当时间到或者调**notify()**方法唤醒它,继续执行下去;第二种,调用**sleep(long time)**方法并加入时间参数,进入睡眠状态;当时间结束,继续执行,这种方式在睡眠期间无法被唤醒。

    2.java线程池的核心参数有哪些?工作流程是怎样的?

    核心参数:
    corePoolSize(核心线程数目):最多保留的线程数
    maximumPoolSize(最大线程数目):核心线程+救急线程
    keepAliveTime(生存时间):针对救急线程
    unit(时间单位):针对救急线程
    workQueue(阻塞队列):阻塞队列
    threadFactory(线程工厂):可以为线程创建时起个好的名字
    handler(拒绝策略):
    AbortPolicy:直接抛出一个RejectedExcutionException的RuntimeException异常;(默认)
    DiscardPolicy:直接丢弃,不给任何通知;(不负责任,不可取)
    DiscardOldestPolicy:丢弃任务队列中的头节点。
    CallerRunPolicy:谁提交谁执行。
    自定义拒绝策略:实现RejectedExecutionHandler接口来实现自己的决绝策略(推荐)

线程池的工作流程:
image-20220107152235483.png

  1. 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于corePoolSize,则创建一个新的线程执行该任务;
  2. 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列;
  3. 如果任务队列已满,则任务无法入队列,此时如果当前线程池中线程数小于maxPoolSize,则创建一个临时线程(非核心线程)执行该任务。
  4. 如果当前池中线程数已经等于maxPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

注意:
当池中线程数大于corePoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是救急线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。

并发场景:
场景一:并发任务小于等于核心任务数情况;
线程池初始化核心线程数为0(性能考虑,按需加载);当线程池内线程数量未达到核心线程数时,如有新的任务加入,不用复用此时的空闲线程;
场景二:并发任务数大于核心线程数 ,且小于等于核心线程数+任务队列长度
当线程达到核心线程数且任务队列未满时,不会再创建新的线程对象;当全部执行完毕后,依旧会保留核心线程数
场景三:并发任务时,阻塞队列已满,且未达到最大线程数:
当并发任务数量超过核心线程数+任务队列,且小于最大线程数+任务队列时,线程池会主动创建新的线程;超过keepAliveTime设置的时间,超过核心线程的线程对象会被淘汰;
场景四:并发任务数量超过最大线程数+任务队列时的场景;
当并发任务数超过最大线程数+任务队列长度时,触发线程的拒绝策略;

3.创建线程有几种方式(必会)

1)继承Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类。
2)实现Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3)实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4)使用线程池创建(使用java.util.concurrent.Executor接口)。

4.Runnable和Callable的区别?(必会)

Runnable Callable
Runnable 接口 run 方法无返回值 Callable 接口 call 方法有返回值,支持泛型
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理 Callable 接口 call 方法允许抛出异常,可以获取异常信息

5.如何启动一个新线程、调用start和run方法的区别?(必会)

start方法 run方法
线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行 线程对象调用run方法不开启线程,仅是对象调用方法。
调用start方法可以启动线程,并且使得线程进入就绪状态 run方法只是thread的一个普通方法,还是在主线程中执行。

6.线程相关的基本方法?(必会)

线程等待(wait) 调用该方法的线程进入 WAITING 状态,会释放对象的锁。
线程睡眠(sleep) sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占
有的锁,进入 TIMED-WATING 状态。
线程让步(yield) yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。
线程中断(interrupt) 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的 一个中断标识位。

Join 等待其他线程终止 | 等待其他线程终止,在当前线程中调用一个线程的 join() 方 法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。 | | | 线程唤醒(notify) | Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果存在多个线程则会选择唤醒其中一个线程,选择是任意的。notifyAll() ,唤醒再次监视器上等待的所有线程。 | |

7.wait()和sleep()的区别?(必会)


wait( ) sleep( )
来自不同的类 wait():来自Object类 sleep():来自Thread类
锁的释放 wait():在等待的过程中会释放锁 sleep():在等待的过程中不会释放锁
使用范围 wait():必须在同步代码块/同步方法中使用 **sleep():可以在任何地方使用;

| | 是否需要捕获异常 | wait():不需要捕获异常; | sleep():需要捕获异常;** |

8.CAS原理

CAS:Compare And Swap 比较并交换,是硬件层面的原子操作
CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
ABA问题
如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
解决方案:AtomicStampedReference

集合篇

1.List 和 Map、Set 的区别(必会)

List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合
List中存储的数据是有顺序的,并且值允许重复;
Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;
Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。

2.List 和 Map、Set 的实现类(必会)*

|

List(有序,可重复) | **ArrayList

优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
**Vector

| 优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全,效率低, 已给舍弃了 | | | | **LinkedList

| 优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高 | | |

Set(无序,唯一) | HashSet | 底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?
依赖两个方法:hashCode()和equals() | | | |

**LinkedHashSet

** | 底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一 | | | |

**TreeSet

** | 底层数据结构是红黑树。(唯一,有序)
1. 如何保证元素排序的呢?
自然排序;比较器排序
2.如何保证元素唯一性的呢?
依赖两个方法:hashCode()和equals() | | |

Map(双列数据集合) | HashMap | 基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键, 线程不安全 | | | | HashTable | 线程安全,低效,不支持 null 值和 null 键; | | | | LinkedHashMap | 线程不安全,是 HashMap 的一个子类,保存了记录的插入顺序; | | | | TreeMap | 能够把它保存的记录根据键排序,默认是键值的升序排序,线程不安全。 | |

3.HashMap和HashTable ConcurrentHashMap区别(高薪常问)*

HashMap HashTable ConcurrentHashMap
HashMap 是非线程安全的 HashTable 是线程安全的 ConcurrentHashMap是线程安全的
HashMap 的键和值都允许有 null 值存在 HashTable 不行 ConcurrentHashMap不行
效率比HashTable高 效率低 效率最高
HashMap 不是,更适合于单线程环境 Hashtable 是同步的使用的是 Synchronized 关键字修饰,是被遗弃的类。 ConcurrentHashMap 是JDK1.7使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap取消了Segment分段锁,采用CAS算法和synchronized来保证并发安全。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率提升N倍

4.HashMap的面试点*

底层数据结构,1.7与1.8有何不同?
1.7是数组+链表,1.8是数组+(链表(变量node)|红黑树(treenode))
桶下标是对元素计算哈希值对对度取模运算,比如当前数组长度为16,哈希值为97,索引下标为1,引入哈希表就是为了提高查询效率。
当元素个数超过数组长度的4/3,数组就会扩容,长度是原来的两倍。扩容之后也会减少链表的长度。注意:当n个元素的哈希值一样,就算扩容他们的链表长度还是不会减少。

为何要用红黄黑树,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?
红黑树主要是用来避免DOS攻击,防止链表超长时性能下降,树化应当是偶然情况:

  1. - **hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间复杂度是O(**logn**),Treenode占用空间也比普通Node的大,如非必要,尽量还是使用链表。**
  2. - **hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006(亿分之六),选择8就是为了让树化的几率足够小**

树化的两个条件:链表长度超过树化阈值;数组容量大于等于64,链表长度是有可能超过8的。。
退化情况1:在扩容时如果拆分树时,树元素个数小于等于6则会退化为链表;
退化情况2:remove树节点时,若root、root.left、root.right、root.left.left有一个为null,也会退化为链表。注意:是在移除之前进行检查的。

索引如何计算?hashCode都有了,为何还要提供hash( )方法?数组容量为何是2的N次幂?

  • 计算对象的hashCode( ),在进行调用HashMap的hash( )方法进行二次哈希,最后&(capacity-1)得到索引下标;注意:位与运算比普通取模运算效率更高,但是capacity必须是2的N次幂。
  • 二次hash( )是为了综合高位数据,让哈希分布更为均匀;hash^hash>>>16
  • 计算索引时,如果是2的N次幂可以使用位与运算代替取模,效率更高;扩容时hash(原始哈希值)&oldCap(旧数组长度)==0的元素留在原来的位置,否则新位置=旧位置+oldCap(旧数组长度);
  • 但以上三点都是为了配合容量为2的N次幂时的优化手段,例如HashTable的容量就不是2的N次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用2的N次幂作为容量。
    • 如果追求分布更加均匀,数组容量应该选择质数,如果追求性能数组容量就要选择2的N次幂。

介绍一下put方法流程,1.7与1.8有何不同?
put的流程:

  1. HashMap是懒惰创建数组的,首次使用才创建数组;
  2. 计算索引(桶下标);
  3. 如果桶下标还没人占用,创建Node占位返回;
  4. 如果桶下标已经有人占用:
    1. 已经是TreeNode节点走红黑树的添加或更新逻辑;
    2. 是普通Ndoe节点,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑;
  5. 返回前检查容量是否超过阈值,一旦超过进行扩容。

注意:HashMap的扩容是在添加元素以后,才进行扩容的。
1.7与1.8的不同:

  1. 链表插入节点时,1.7是头插法,1.8是尾插法;
  2. 1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容;
  3. 1.8在扩容计算node索引时,会优化。

加载因子为何默认是0.75f?

  1. 在空间占用与查询时间之间取得较好的权衡;
  2. 大于这个值,空间节省了,但链表就会比较长会影响性能;
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用多。

多线程下会有啥问题?

  1. 1.7会出现扩容死链
  2. 1.7和1.8都会出现数据错乱

key能否为null,作为key的对象有什么要求?

  1. HashMap的key可以为null,但Map的其他实现则不然;
  2. 作为key的对象,必须实现hashCode(让哈希分布更加均匀,增加查询效率)和equals(当key的哈希值一样时,使用equals比较对象的内容是否一样),并且key的内容不能修改(不可变)。

String对象的hashCode( )如何设计的,为啥每次乘的是31?
目标是达到均匀的散列效果,每个字符串的hashCode足够独特。

  1. 字符串中的每个字符都可以表现为一个数字,称为Si,其中i的范围是0~n-1;
    • 散列公式为:
    • javaSE基础面试题 - 图3
  2. 31代入公式有较好的散列特性,并且31*h可以被优化为:
    1. 即32*h-h
    2. 即2的5次幂*h-h
    3. 即h<<5-h

      5.请简要谈谈ArrayList*

  • 1.ArrayList是基于数组实现的,它的内部存储元素的数组为 elementData;elementData的声明为:transient Object[] elementData;查询快,增删慢;
  • 2.ArrayList中EMPTYELEMENTDATADEFAULTCAPACITYEMPTY_ELEMENTDATA的使用;这两个常量,使用场景不同。前者是用在用户通过ArrayList(int initialCapacity)该构造方法直接指定初始容量为0时,后者是用户直接使用无参构造创建ArrayList时
  • 3.ArrayList默认容量为10。调用无参构造新建一个ArrayList时,它的elementData = DEFAULTCAPACITYEMPTYELEMENTDATA, 当第一次使用 add() 添加元素时,ArrayList的容量会为 10。
  • 4.ArrayList的扩容计算为 newCapacity = oldCapacity + (oldCapacity >> 1);且扩容并非是无限制的,有内存限制,虚拟机限制。
  • 5.ArrayList的toArray()方法subList()方法,在源数据子数据之间的区别;
  • 6.注意扩容方法ensureCapacityInternal()。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则不建议使用。

    6.请简要谈谈LinkedList的底层实现*

    添加节点:

  • 1.记录当前末尾节点,通过构造另外一个指向末尾节点的指针l

  • 2.产生新的节点:注意的是由于是添加在链表的末尾,next是为null的
  • 3.last指向新的节点
  • 4.这里有个判断,我的理解是判断是否为第一个元素(当l==null时,表示链表中是没有节点的), 那么就很好理解这个判断了,如果是第一节点,则使用first指向这个节点,若不是则当前节点的next指向新增的节点
  • 5.size增加 例如,在上面提到的LinkedList[“A”,”B”,”C”]中添加元素“D”,过程大致如图所示

LinkedList添加元素图解.jpg
删除节点

  • 1.获取到需要删除元素当前的值,指向它前一个节点的引用,以及指向它后一个节点的引用。
  • 2.判断删除的是否为第一个节点,若是则first向后移动,若不是则将当前节点的前一个节点next指向当前节点的后一个节点
  • 3.判断删除的是否为最后一个节点,若是则last向前移动,若不是则将当前节点的后一个节点的prev指向当前节点的前一个节点
  • 4.将当前节点的值置为null
  • 5.size减少并返回删除节点的值

    12.ArrayList与LinkedList的区别是什么?

    |
    | ArrayList | LinkedList | | —- | —- | —- | | 相同点 | 接口实现:都实现了List接口,都是线性列表的实现
    都是线程不安全的 | | |

不同点 | ArrayList内部是数组实现 | LinkedList是内部实现是双向链表结构 | | | ArrayList实现了RandomAccess可以支持随机访问 | LinkedList实现了Deque可以当做队列使用 | | | 新增、删除元素时ArrayList需要使用到拷贝原数组,查找元素时ArrayList支持随机元素访问 | 新增、删除元素时LinkedList只需要移动指针,查找元素时LinkedList只能一个节点的去遍历 |