# 面向对象

# 面向对象的三大特征

面向对象的程序设计方法具有三个基本特征:封装、继承、多态。
封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;
继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。

# Java 基础

# String & StringBuilder & StringBuffer

String 类是不可变类,即一旦一个 String 对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
StringBufferStringBuilder 都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder ,并且两个类的构造方法和成员方法也基本相同。不同的是, StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,所以 StringBuilder 性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑 StringBuilder 类。

# Java 中 str = "hello"str = new String("hello') 两种创建字符串的区别

当 Java 程序直接使用 "hello" 的字符串直接量时,JVM 会使用常量池来管理字符串直接量 (在执行这句话时,JVM 会先检查常量池中是否已经存有 "hello" ,若没有则将 "hello" 存入常量池,否则就复用常量池中已有的 "hello" ,将其引用赋值给变量 str。)。

当使用 new String("hello") 时,JVM 会先使用常量池来管理 "hello" 字符串直接量,再调用 String 类的构造器来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。

# Java 创建对象的方式

  1. 使用 new 关键字
  2. 使用 Class 类的 newInstance () 方法:
    1. 使用 Class 类的 forName () 方法
    2. 使用 ClassLoader 类的 loadClass () 方法
  3. 使用 Constructor 类的 newInstance () 方法
  4. 使用 clone () 方法
  5. 使用反序列化

# 两个字符串相加的底层实现

如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
如果拼接的字符串中包含变量,则在编译时编译器采用 StringBuilder 对其进行优化,即自动创建 StringBuilder 实例并调用其 append () 方法,将这些字符串拼接在一起。

# Java 的异常接口

Throwable 是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是 Error、Exception。

  • Error 是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用 catch 块来捕获 Error 对象。在定义方法时,也无须在其 throws 子句中声明该方法可能抛出 Error 及其任何子类。
  • Exception 是异常,它被分为两大类,分别是 Checked 异常和 Runtime 异常。
    • RuntimeException (不受检异常): RuntimeException 类及其子类表示 JVM 在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
    • CheckedException (受检异常): Exception 中除 RuntimeException 及其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说 IOException ,必须对该异常进行处理,要么使用 try-catch 捕获,要么使用 throws 语句抛出,否则编译不通过。

# Java 对象的四种引用方式

Java 对象的四种引用方式分别是强引用、软引用、弱引用、虚引用,具体含义如下:

  • 强引用:这是 Java 程序中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
  • 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
  • 弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
  • 虚引用:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

# 容器类型

# Java 中有哪些容器

Java 中的集合类主要由 CollectionMap 这两个接口派生而出,其中 Collection 接口又派生出三个子接口,分别是 SetListQueue 。所有的 Java 集合类,都是 SetListQueueMap 这四个接口的实现类,这四个接口将集合分成了四大类,其中

  • Set 代表无序的,元素不可重复的集合;
  • List 代表有序的,元素可以重复的集合;
  • Queue 代表先进先出(FIFO)的队列;
  • Map 代表具有映射关系(key-value)的集合。

# Java 中容器的线程安全性

java.util 包下的集合类大部分都是线程不安全的,例如我们常用的 HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的集合类,但是它们的优点是性能好。如果需要使用线程安全的集合类,则可以使用 Collections 工具类提供的 synchronizedXxx() 方法,将这些集合类包装成线程安全的集合类。

java.util 包下也有线程安全的集合类,例如 Vector、Hashtable 。这些集合类都是比较古老的 API ,虽然实现了线程安全,但是性能很差。所以即便是需要使用线程安全的集合类,也建议将线程不安全的集合类包装成线程安全集合类的方式,而不是直接使用这些古老的 API 。

从 Java5 开始,Java 在 java.util.concurrent 包下提供了大量支持高效并发访问的集合类,它们既能保证良好的访问性能,又能保证线程安全。这些集合类可以分为两部分,它们的特征如下:

  • 以 Concurrent 开头的集合类:
    以 Concurrent 开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以 Concurrent 开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
  • 以 CopyOnWrite 开头的集合类:
    以 CopyOnWrite 开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

# HashMap

# HashMap 为什么在获取 hash 值时要进行位运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 如果使用直接使用 hashCode 对数组大小取余,那么相当于参与运算的只有 hashCode 的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode 取值出的高位也参与运算,进一步降低 hash 碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。
  2. 而这么来看 hashCode 被散列 (异或) 的是低 16 位,而 HashMap 数组长度一般不会超过 2 的 16 次幂,那么高 16 位在大多数情况是用不到的,所以只需要拿 key 的 HashCode 和它的高 16 位做异或即可利用高位的 hash 值,降低哈希碰撞概率也使数据分布更加均匀。

为什么用异或
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash () 返回值就会改变。尽可能的减少碰撞。

# JDK7 和 JDK8 中 HashMap 的区别

JDK7 中的 HashMap ,是基于数组 + 链表来实现的,它的底层维护一个 Entry 数组。它会根据计算的 hashCode 将对应的 KV 键值对存储到该数组中,一旦发生 hashCode 冲突,那么就会将该 KV 键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。JDK7 中 HashMap 的实现方案有一个明显的缺点,即当 Hash 冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为 O (N) 。

JDK8 中的 HashMap ,是基于数组 + 链表 + 红黑树来实现的,它的底层维护一个 Node 数组。当链表的存储的数据个数大于等于 8 的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为 O (N) ,而红黑树一直是 O (logN) ,可以大大的提高查找性能。

# HashMap 扩容

JDK1.7 扩容
JDK1.7 发生扩容的条件必须同时满足两点:

  • 当前存储的数量大于等于阈值
  • 发生 hash 碰撞

特点:先扩容,再添加(扩容使用的头插法)
缺点:头插法会使链表发生反转,多线程环境下可能会死循环
扩容结果:table 容量变为 2 倍,所有的元素下标需要重新计算,newIndex = hash (扰动后) & (newLength - 1)

JDK1.8 扩容
JDK1.8 发生扩容的条件需满足其中一项:

  • 当前存储的数量大于等于阈值
  • 当某个链表长度 >=8,但是数组存储的结点数 size () < 64 时

特点:先插后判断是否需要扩容(扩容时是尾插法)
缺点:多线程下,1.8 会有数据覆盖
table 容量变为 2 倍,但是不需要像之前一样计算下标,只需要将 hash 值和旧数组长度相与即可确定位置。

  • 如果 Node 桶的数据结构是链表会生成 low 和 high 两条链表,是红黑树则生成 low 和 high 两颗红黑树
  • 依靠 (hash & oldCap) == 0 判断 Node 中的每个结点归属于 low 还是 high。
  • 把 low 插入到 新数组中 当前数组下标的位置,把 high 链表插入到 新数组中 [当前数组下标 + 旧数组长度] 的位置
  • 如果生成的 low,high 树中元素个数小于等于 6 退化成链表再插入到新数组的相应下标的位置

# 链表升级成红黑树的条件

链表长度大于 8 时才会考虑升级成红黑树,是有一个条件是 HashMap 的 Node 数组长度大于等于 64(不满足则会进行一次扩容替代升级)。

# 红黑树退化成链表的条件

  • 扩容 resize ( ) 时,红黑树拆分成的 树的结点数小于等于临界值 6 个,则退化成链表。
  • 删除元素 remove () 时,在 removeTreeNode () 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树 / 右子树为空,或者 根的左子树的左子树为空,都会发生红黑树退化成链表。
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }

# ConcurrentHashMap

# JDK 1.7 中的实现:

在 jdk 1.7 中,ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成,采取分段锁来保证安全性。Segment 是 ReentrantLock 重入锁,在 ConcurrentHashMap 中扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 类似,是一个数组和链表结构。
ConcurrentHashMap-jdk1.7
JDK1.7 中的并发方式只是相当于在一个 HashMap 中包含多个 HashMap ,在对不同 HashMap 的访问上能够提供并发,并发度取决于 Segment 数组的长度。

# JDK 1.8 中的实现:

JDK1.8 的实现已经摒弃了 Segment 的概念,而是直接用 Node 数组 + 链表 + 红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
ConcurrentHashMap-jdk1.8

# LinkedHashMap

  • LinkedHashMap 使用双向链表来维护 key-value 对的顺序(其实只需要考虑 key 的顺序),该链表负责维护 Map 的迭代顺序,迭代顺序与 key-value 对的插入顺序保持一致。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表重写了部分方法。
  • LinkedHashMap 可以避免对 HashMap 、Hashtable 里的 key-value 对进行排序(只要插入 key-value 对时保持顺序即可),同时又可避免使用 TreeMap 所增加的成本。
  • LinkedHashMap 需要维护元素的插入顺序,因此性能略低于 HashMap 的性能。但因为它以链表来维护内部顺序,所以在迭代访问 Map 里的全部元素时将有较好的性能。

# CopyOnWriteArrayList

CopyOnWriteArrayList 允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。

private E get(Object[] a, int index) {
        return (E) a[index];
    }
    public boolean add(E e) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }
    }
    public E remove(int index) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        }
    }

# BlockingQueue

实现类 功能
ArrayBlockingQueue 基于数组的阻塞队列,使用数组存储数据,并需要指定其长度,所以是一个有界队列
LinkedBlockingQueue 基于链表的阻塞队列,使用链表存储数据,默认是一个无界队列;也可以通过构造方法中的 capacity 设置最大元素数量,所以也可以作为有界队列
SynchronousQueue 一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费
PriorityBlockingQueue 基于优先级别的阻塞队列,底层基于数组实现,是一个无界队列 DelayQueue 延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队

声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:如果采用公平模式:SynchronousQueue 会采用公平锁,并配合一个 TransferQueue 来阻塞多余的生产者和消费者,从而体系整体的公平策略;但如果是非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 TransferStack 来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

# Thread

# 进程与线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

# 创建线程的方式

创建线程有三种方式,分别是继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。

  • 通过继承 Thread 类来创建并启动线程的步骤如下:
    • 定义 Thread 类的子类,并重写该类的 run () 方法,该 run () 方法将作为线程执行体。
    • 创建 Thread 子类的实例,即创建了线程对象。
    • 调用线程对象的 start () 方法来启动该线程。
  • 通过实现 Runnable 接口来创建并启动线程的步骤如下:
    • 定义 Runnable 接口的实现类,并实现该接口的 run () 方法,该 run () 方法将作为线程执行体。
    • 创建 Runnable 实现类的实例,并将其作为 Thread 的 target 来创建 Threa d 对象,Thread 对象为线程对象。
    • 调用线程对象的 start () 方法来启动该线程。
  • 通过实现 Callable 接口来创建并启动线程的步骤如下:
    • 创建 Callable 接口的实现类,并实现 call () 方法,该 call () 方法将作为线程执行体,且该 call () 方法有返回值。然后再创建 Callable 实现类的实例。
    • 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call () 方法的返回值。
    • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
    • 调用 FutureTask 对象的 get () 方法来获得子线程执行结束后的返回值。

# run() & start()

run () 方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而 start () 方法用来启动线程。
start () 方法用于启动线程,run () 方法用于执行线程的运行时代码。但如果直接调用线程对象的 run () 方法,则 run () 方法立即就会被执行,而且在 run () 方法返回之前其他线程无法并发执行。run () 可以重复调用,而 start () 只能调用一次。

# Thread 是否能重新启动

只能对处于新建状态的线程调用 start () 方法,否则将引发 IllegalThreadStateException 异常。
如果想重用一个过程,可以把线程逻辑定义在 Runnable 对象里面,通过新建 Thread 对象达到重新启动逻辑的效果。

# Thread 的五种状态

在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
线程 5 种状态的转换关系

# 产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

# sleep () 和 wait () 有什么区别

  • 类的不同:sleep () 来自 Thread,wait () 来自 Object。
  • 释放锁:sleep () 不释放锁;wait () 释放锁。
  • 用法不同:sleep () 时间到会自动恢复;wait () 可以使用 notify () /notifyAll () 直接唤醒。

# 锁升级

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

# ThreadPool

# 线程池的参数

public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于 corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于 corePoolSize 时。
  • maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于 maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  • workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
  • threadFactory(线程创建工厂):用于创建新线程。threadFactory 创建的线程也是采用 new Thread () 方式,threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n(m 为线程池的编号,n 为线程池内的线程编号)。
  • handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略

# 默认线程池

线程池名称 使用阻塞队列 特点
newFixedThreadPool LinkedBlockingQueue() 1、核心线程数和最大线程数相同。2、由于 keepAliveTime 设置为 0 ,当线程创建后会一直存在。3、由于用的是无界队列所以可能会导致 OOM。
newSingleThreadExecutor LinkedBlockingQueue() 1、核心线程数和最大线程数都为 1 ,单线程。2、无界队列可能导致 OOM 。
newCachedThreadPool SynchronousQueue() 1、核心线程数为 0 ,最大线程数为 Integer.MAX_VALUE 。2、当没任务时线程存活时间为 60 秒。3、使用的是 0 大小的队列,所以不存储任务,只做任务转发。4、极端情况下会创建过多的线程,耗尽 CPU 和内存资源。
newScheduledThreadPool DelayedWorkQueue() 1、执行周期任务。2、无界队列,可能会导致 OOM 。

# 线程池工作原理

线程池工作原理

# 线程池的拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到 maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。
  • DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
  • CallerRunsPolicy:由调用线程处理该任务。