面向对象编程 OOP
继承
继承 extends
定义子类时,若不显式调用 super 方法,就调用父类的无参构造函数
抽象
抽象类 abstract
- 抽象类才可以包含被
abstract修饰的抽象方法,且在被继承时必须实现这些抽象方法; - 尽管抽象类不能被实例化出一个对象,但可以提供具体方法用于被继承;
如果抽象方法没有被全部实现,则该子类也必须被
abstract修饰成为抽象类。接口
interface接口不是类,是对符合这个接口的类的一系列需求,因此不能实例化;
- 一个类可以实现多个接口来达到多继承的目的;
如果在接口中需要提供默认方法实现,可以使用
default修饰。抽象类与接口的异同
相同点:
- 不能被实例化;
- 需要将实现方法实现才可以被实例化。
不同点:
重写(包括覆写 Override 和重载 Overload)
- 接口
-
重写
覆写 Override
子类对父类允许访问方法的实现过程进行重写,运行时多态。
- 继承;
- 重写;
- 父类引用指向子类实例(
Father son = new Son())。Java 传参方式:传值
除了基础数据类型,变量保存的是对象的引用。
当函数参数为对象时,指向对象的变量(对象的引用) 原始变量引用被复制出了一个新变量并用于函数内部操作,这实际上就是传值;复制的变量与原始变量指向的对象是一样的,因此对参数变量的操作会改变原对象的内容,造成外部看来貌似是传引用,实则不是。基础语法
final
- 修饰字段:调用构造函数时进行初始化;
- 修饰方法:子类不允许
@Override该方法; 修饰类:不允许被
extends,所有方法自动被final修饰,字段不一定。static与类相关,与对象无关。
修饰字段:所有该类实例化的对象都共享该数据;
- 修饰方法:不存在
this指针,只能访问被static修 饰的字段,可以被extends不能被@Override; -
static与继承```java static class A { public static void test(A a) {
System.out.println("A");
}
public static void test(B b) {
System.out.println("B");
} }
static class B extends A {
}
public static void main(String[] args) { A a = new B(); A.test(a); B.test(a); }
输出:```bashAA
解释:
- 两个
test()方法被重载; - 用子类 B 去构造一个实例,并将其引用地址赋值给类型为 A 的 a。调用被
static修饰的方法时,该方法不允许被重写,另外A.test(a)的输出根据参数类型 A 而非具体引用类型去匹配方法,因此输出为 “A”; 用子类 B 继承了父类 A 的
static方法,方法不允许被重写,同样根据参数类型判断方法,因此输出为 “A”。static对类型推断的影响一般情况下,实例调用哪一个方法,根据实例自身是哪一个对象的引用,不需要看实例属于哪个类;
当方法被
static修饰后,这个方法与实例无关,无法通过引用判断出需要调用哪个具体方法,因此被static修饰的方法不能被覆写,只能被继承。代码块
{}代码块加载类时,在构造函数之前执行,按定义的先后顺序执行,每次
new一个实例就执行一次。static {}代码块加载类时,在构造函数之前执行,按定义的先后顺序执行,只在第一次
new实例时执行一次。代码块执行顺序
不管有没有后续操作,加载了类就执行类中定义的静态代码块
new 实例的顺序:静态代码块 -> 非静态代码块 -> 构造函数;
- 不 new 实例的顺序:静态代码块 -> 方法 -> 方法中的代码块。
集合
| 类型 | 具体实现 | 数据结构 | 线程安全 | 特点 |
|---|---|---|---|---|
| List | ArrayList |
数组 | ❌ | - 查询快,增删慢 |
Vector |
✅ | |||
LinkedList |
双向链表 | ❌ | - 查询慢,增删快 - 可以当作堆、栈、队列或双向队列的实现 |
|
| Map | HashMap |
数组 + 链表/红黑树 | ❌ | - key 和 value 可以为 null- JDK 8 之后使用数组 + 链表/红黑树,链表内元素超过 8 后转化为红黑树 |
ConcurrentHashMap |
✅ | - 使用分段锁满足线程安全性,继承自 ReentrantLock- JDK 8 之后同样添加了红黑树 |
||
HashTable |
数组 + 链表 | - 性能不如 ConcurrentHashMap,基本已废弃 |
||
TreeMap |
数组 + 红黑树 | ❌ | - key 需要实现 Comparable 接口或自定义比较器- 实现了 SortedMap 接口保证了按 key 排序,因此 key 和 value 都不允许为 null |
|
LinkedHashMap |
数组 + 链表 | - 保证链表上数据的插入顺序 |
||
| Set | HashSet |
数组 | ❌ | - 存放 HashCode |
TreeSet |
二叉树 | - 按顺序存放对象 - 需要实现 Comparable 接口并覆写 compareTo 方法 |
||
LinkHashSet |
双向链表 | - 继承 HashSet |
||
| Queue | ArrayBlockingQueue |
数组 | ✅ | - 有界阻塞队列 |
LinkedBlockingQueue |
链表 | |||
LinkedTransferQueue |
- 无界阻塞队列 |
|||
LinkedBlockingDeque |
- 双向阻塞队列 |
|||
PriorityBlockingQueue |
堆 | - 按优先级排序的无界阻塞队列 |
||
DelayQueue |
- 支持延迟操作的无界阻塞队列 |
|||
SynchronousQueue |
链表 | - 用于线程同步的阻塞队列 |
HashMap
作为 key 的类重写 equals 和 hashCode 方法
- 重写了
equals但没重写hashCode:
两值个相等的对象可能算出来的哈希值不同,因此被安放在了两个不同的桶中,造成 key 值相等的对象存在两个的问题。
- 重写了
hashCode但没重写equals:
equals 比较两个对象的地址,只要不是同一个对象必为 false,因此值相等的两个 key 发生哈希碰撞后不会覆盖,而是存在两个值相等的 key。
HashCode 的生成
jvm 使用不同的参数
-XX:hashCode=<0~5>选择不同的 HashCode 生成算法:
- 直接返回随机数(高并发时会自旋等待)。
- 对象的内存地址位移 + 异或。
- 固定返回 1(用于测试)。
- 随着对象个数自增。
- 直接返回对象地址。
- 当前状态进行异或(默认)。
哈希冲突
HashMap 由 Entry 组成的数组(桶)构成,当 Entry 的 HashCode 冲突时,同一个 HashCode 下再用链表构成;链表的元素插入方式是头插;链表长度大于 8 时,转化为红黑树保存数据;
链表自动扩展时长度为 2 的整数次幂(为了 hash 后减少碰撞);
loadFactor 为 0.75(泊松分布,碰撞最小的扩展方式,时间和空间的折衷)。
使用红黑树的原因
普通的二叉排序树,在最坏情况下可能退化为线性结构;
平衡二叉树(AVL 树),追求完全平衡,插入节点产生旋转的时间复杂度是,但删除节点产生旋转的时间复杂度为
;
红黑树,不追求完全平衡,任何不平衡在三次旋转内可以解决,时间复杂度为。
AVL 查找效率高,但红黑树则是查找、插入、删除效率上的折衷。
并发编程
线程
线程创建与使用
| 方法 | 描述 | 代码 |
|---|---|---|
继承 Thread 类 |
对不需要继承的类使用,start 方法是一个 native 方法,在 OS 中启动一个新线程后执行实现的 run 方法 |
```java |
public class ClassThread extends Thread { public void run() { // 线程具体任务 }
public static void main(String[] args) {ClassThread ct = new ClassThread();// start 方法启动线程ct.start();}
}
|| 实现 `Runnable` 接口 | 对需要继承的类使用,直接实现 `run` 方法,再通过该类的实例去创建一个 `Thread` 对象,调用该对象的 `start` 方法 | ```javapublic class ClassThread extends SuperClass implements Runnable {public void run() {// 线程具体任务}public static void main(String[] args) {ClassThread ct = new ClassThread();// 使用已有实例创建线程对象Thread t = new Thread(ct);// start 方法启动线程t.start();}}
| | 基于线程池 | ThreadFactory 的创建方式 | ```java public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); for(int i = 0; i < 5; i++) { pool.execute(new Runnable() { @Override public void run() { // 线程具体任务 } }); } }
|| `ExecutorService` 与 `Callable<Class>` | 在类中实现 `Callable` 接口,在 `call` 方法中定义线程具体任务,再通过线程池提交任务的方式将执行结果保存在 `List<Future>` 中 | ```javapublic class ClassCallable implements Callable<String> {private String data;public ClassCallable(String data) {this.data = data;}@Overridepublic String call() throws Exception {// 线程具体任务并返回结果return data;}public static void main(String[] args) {// 创建线程池ExecutorService pool = Executors.newFixedThreadPool(5);// 保存结果的 ListList<Future> results = new ArrayList<>();for(int i = 0; i < 5; i++) {// 创建带返回值的线程实例Callable c = new ClassCallable(i + "");// 保存线程池执行结果Future f = pool.submit(c);results.add(f);}// 关闭线程池pool.shutdown();}}
|
线程状态
线程可用方法

- 等待
wait:释放锁,进入 WAITING 状态,需要等待唤醒(notify)或中断(interrupt)才有返回。 - 睡眠
sleep:不释放锁,进入 TIMED_WAITING 状态,需要等待固定时长。 - 放弃
yield:让出 CPU 并重新竞争时间片。 - 中断
interrupt:只改变标识位,不会改变线程状态,Running 状态不会终止/TIMED_WAITING 状态提前结束。 - 加入
join:阻塞一个线程到被join的线程结束,再转为Runnable 状态重新竞争时间片。 - 唤醒
notify:唤醒在某个对象上等待的线程(任选其一,如果全部线程都要唤醒则使用notifyAll方法),一般在wait方法后使用。 - 护线程
setDaemon:定义一个守护线程。守护线程(服务线程):
- 为用户线程提供公共服务,没有用户线程时自动离开,如垃圾回收线程;
- 周期性地执行某些任务或处理事件;
- 与 JVM 同生共死(因此所有线程都是守护线程时 JVM 就可以退出了)
方法对比
sleep 与 wait
Thread.sleep()和Object.wait()的关系;sleep不释放锁,wait释放锁;sleep肯定会恢复运行状态,wait不被 notify 不会进入运行状态。start与runstart启动线程,进入 Runnable 状态但不是 Running 状态;run方法包含线程任务逻辑,只有调用了该方法才进入 Running 状态。无锁情况下
notifywait/notify/notifyAll必须保证调用的对象是同步的(处于synchroized(object)语句块内部),否则会报错。
⚠️ 需要先notify后wait,原因:被wait后的线程不再是同步的,不持有锁了。wait:阻塞当前线程,释放所有锁,使用前提是已经获得了锁(进入同步状态);notify:唤醒等待状态的线程,不会立即释放锁,因此尽量调用后退出临界区,让其他线程获得锁;notifyAll:唤醒全部等待状态的线程。线程池

目的
- AbortPolicy:直接丢弃,抛出
RejectedExecutionException异常; - DiscardPolicy:直接丢弃;
- DiscardOldestPolicy:丢弃等待队列中最老的未处理任务,将被拒绝的任务添加到等待队列;
- CallerRunsPolicy:不进入线程池,由调用者线程执行任务。
锁
锁分类
乐观锁与悲观锁
乐观锁通过 CAS(Compare And Swap)原子更新操作实现,在对数据操作之前会比较当前值(本地内存)与传入值(全局内存),若不同则更新数据。相当于没有加锁的过程。具体实现如:
java.util.concurrent包下的原子操作。- CAS 无锁算法:
包含的操作数:需要读写的内存值 V;进行比较的值 A;要写入的新值 B。 当且仅当
V == A时,CAS 通过原子方式用新值 B 来更新 V 的值(“比较 + 更新”整体是一个原子操作),否则不会执行任何操作。 一般情况下,“更新”是一个不断重试的操作。- CAS 局限性:
- ABA 问题:内存中原来是 A,后来变成了 B,然后又变成 A,那么 CAS 检查时认为值没有发生变化。
解决思路:在变量前添加版本号,每次变量更新就把版本号 +1,变化过程则为“1A -> 2B -> 3A”。 JDK 1.5 开始提供
AtomicStampedReference解决 ABA 问题,具体操作封装在compareAndSet方法中。- 循环时间长开销大:CAS 操作长时间不成功会导致一直自旋,给 CPU 带来非常大的开销。
- 只保证一个共享变量的原子操作:对多个共享变量操作时,CAS 无法保证操作的原子性。
JDK 1.5 开始提供
AtomicReference来保证引用对象之间的原子性,可以把多个变量放在一个对象里进行 CAS 操作。悲观锁通过 AQS(Abstract Queued Synchronized)实现,在操作数据前就加锁让其他需要该数据的线程阻塞。会先尝试乐观锁,若获取不到进入悲观锁。具体实现如:
synchronized、Lock具体实现等。自旋锁

自旋锁为了减少 CPU 上下文切换和恢复现场所带来的性能损耗而出现,方式是不放弃 CPU 时间片不断去尝试获取锁,进入一个“自旋”的状态。其自旋的原理也是 CAS,通过循环不断尝试修改数据。自旋锁在 JDK 1.6 以后采用自适应自旋锁,自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,简单来说就是成功获取锁的次数越多自旋允许的时间越长。
无锁、偏向锁、轻量级锁与重量级锁
无锁、偏向锁、轻量级锁与重量级锁都是锁的状态,存在于关键字 synchronized 中。
Java 对象头
Hotspot 虚拟机中 Java 对象头包含两个部分:
- Class Pointer:指向类元数据的指针,用来确定对象是哪个类的实例,是“对象无关类相关”的信息。
- Mark Word:存储对象的 HashCode,分代年龄和锁标志位这样的“对象相关类无关”信息。
Monitor
Monitor 是一种同步工具,是一个对象,每个 Java 对象都存在一个看不见的 Monitor 锁。 每一个被锁住的对象都会和一个 Monitor 关联,同时 Monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。 Monitor 的底层实现依赖于操作系统的Mutex Lock(互斥锁)。
- 无锁:CAS 的流程,所有线程都可以对数据修改,但同一时刻只有一个能修改成功。
- 偏向锁:只有一个线程执行同步代码块时避免多次的 CAS 的原子操作(只在切换线程 ID 时进行一次 CAS),提升性能。有其他线程进行竞争时,升级为轻量级锁。
- 轻量级锁:使用自旋锁的方式获取锁,无阻塞。有除了持有轻量级锁和自旋的线程外的第三个线程时,升级为重量级锁。
-
公平锁与非公平锁
构造函数
ReentrantLock(boolean fair)来指定是否公平,默认非公平锁。 公平锁先到先得。
- 非公平锁随机分配。
ReentrantLock加锁的区别:tryLock:无可用锁则直接返回,不等待;lock:无可用锁就一直等待;lockInterruptibly:锁中断时抛出异常,其他加锁方式不会抛出异常。
共享锁与独占锁
- 共享锁允许多个线程同时获取该锁,与乐观锁的 CAS 思路类似,实现有
ReentrantReadWriteLock。 - 独占锁(互斥锁),每次只允许一个线程持有,与悲观锁的 AQS 思路类似,实现有
ReentrantLock。Java 中的实现
| |synchronized|volatile| | —- | —- | —- | | 底层原理 | CAS | 内存可见(CPU总线嗅探机制) | | 可重入性 | 满足 | 无 | | 可见性 | 满足 | 满足 | | 原子性 | 满足 | 不满足 | | 针对场景 |
1. 多个线程同时写共享变量
1. 依赖共享变量某一时刻的值
1. 对原子性有要求时
|
1. 单个线程写共享变量
1. 不依赖某一时刻的值
1. 尽量避免使用
|
synchronized
锁升级
JDK 1.6 之后对 synchronized 进行了优化,锁可以逐步升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁):
- 偏向锁:适用于单线程,如果发生抢占则持有锁的线程被挂起,并升级为轻量级锁;
- 轻量级锁:不会阻塞,执行速度快,但得不到锁单线程进行自旋耗费 CPU,是 CAS 过程;
重量级锁(自旋锁):阻塞,执行时间长,但不会消耗 CPU,是 AQS 过程。
底层实现
同步代码块使用了底层原语的 monitorenter 和monitorexit,同步方法使用 ACC_SYNCHRONIZED。
每个对象拥有一个 monitor,monitor 只能被一个线程拥有(monitor 的进入数为 0 则可进,进入后变为 1,monitor 进入数为 1 则不允许其他线程进入),是可重入的;其他线程申请进入 monitor 只能等待进入数变为 0;
拥有 monitor 的线程才可以执行 monitorexit,进入数会 -1,降到 0 时释放 monitor(以上过程与 AQS 类似)。对象锁与类锁
synchronized(对象锁)管理的是某个类的一个实例,同一个类的两个实例没办法管理,作用域是this.synchronized;static synchronized(类锁)管理的是某个类所有的实例,作用域是clazz.synchronized。volatile底层原理
被volatile关键字修饰的共享变量,当线程操作它时,从主内存拷贝该变量的最新值作为副本;
线程操作副本并写回主内存后,通过 CPU 总线嗅探机制告知其他线程该共享变量被修改过,需要重新从主内存读取(对外看起来似乎是直接操作了主内存中的变量,故称内存可见)。CPU总线嗅探机制:
- 概念:为了实现缓存一致性的机制,由多缓存引起(不是由多核 CPU 引起,这一点与 JMM 的同步问题类似)。
- 工作原理:每个处理器(线程)监听总线上传播的数据(主内存),检查自己缓存(本地内存)的值是否已经过期;如果监听到了修改,会重新将数据存储到缓存(本地内存)中。
