# 面向对象
# 面向对象的三大特征
面向对象的程序设计方法具有三个基本特征:封装、继承、多态。
封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;
继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
# Java 基础
# String & StringBuilder & StringBuffer
String
类是不可变类,即一旦一个 String
对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
StringBuffer
、 StringBuilder
都代表可变的字符串对象,它们有共同的父类 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 创建对象的方式
- 使用 new 关键字
- 使用 Class 类的 newInstance () 方法:
- 使用 Class 类的 forName () 方法
- 使用 ClassLoader 类的 loadClass () 方法
- 使用 Constructor 类的 newInstance () 方法
- 使用 clone () 方法
- 使用反序列化
# 两个字符串相加的底层实现
如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
如果拼接的字符串中包含变量,则在编译时编译器采用 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 中的集合类主要由 Collection
和 Map
这两个接口派生而出,其中 Collection
接口又派生出三个子接口,分别是 Set
、 List
、 Queue
。所有的 Java 集合类,都是 Set
、 List
、 Queue
、 Map
这四个接口的实现类,这四个接口将集合分成了四大类,其中
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); | |
} |
- 如果使用直接使用 hashCode 对数组大小取余,那么相当于参与运算的只有 hashCode 的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode 取值出的高位也参与运算,进一步降低 hash 碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。
- 而这么来看 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 类似,是一个数组和链表结构。
JDK1.7 中的并发方式只是相当于在一个 HashMap 中包含多个 HashMap ,在对不同 HashMap 的访问上能够提供并发,并发度取决于 Segment 数组的长度。
# JDK 1.8 中的实现:
JDK1.8 的实现已经摒弃了 Segment 的概念,而是直接用 Node 数组 + 链表 + 红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
# 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 种状态。
# 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
# 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:由调用线程处理该任务。