面向对象编程 OOP

继承

继承 extends

定义子类时,若不显式调用 super 方法,就调用父类的无参构造函数

抽象

抽象类 abstract

  • 抽象类才可以包含被 abstract 修饰的抽象方法,且在被继承时必须实现这些抽象方法;
  • 尽管抽象类不能被实例化出一个对象,但可以提供具体方法用于被继承;
  • 如果抽象方法没有被全部实现,则该子类也必须被 abstract 修饰成为抽象类。

    接口 interface

  • 接口不是类,是对符合这个接口的类的一系列需求,因此不能实例化;

  • 一个类可以实现多个接口来达到多继承的目的;
  • 如果在接口中需要提供默认方法实现,可以使用 default 修饰。

    抽象类与接口的异同

  • 相同点:

    • 不能被实例化;
    • 需要将实现方法实现才可以被实例化。
  • 不同点:

    • 接口除非有 default 修饰,否则不允许实现任何方法,抽象类可以实现一些方法;
    • 类可以实现多个接口,但只能继承一个抽象类;
    • 接口强调功能实现,抽象类强调继承关系;
    • 接口中字段默认被 public static final 修饰因此必须赋值,抽象类的字段不能被 privatestaticsynchronizednative 等修饰,可以被修改。

      多态

      多态的实现方式:
  • 重写(包括覆写 Override 和重载 Overload)

  • 接口
  • 抽象类与抽象方法

    重写

    覆写 Override

    子类对父类允许访问方法的实现过程进行重写,运行时多态。

    • 参数个数与类型:不能改变;
    • 返回值类型:可以不同,但必须是父类返回值类型的子类型;
    • 方法访问权限:不能比父类更低,比如父类是 public,重写后不能为 protected
    • finalstatic 修饰的方法不能被重写;
    • 构造函数不能被重写。

      重载 Overload

      相同函数名称使用不同的参数列表,编译时多态。

    • 参数个数与类型:必须不同;

    • 返回值类型:随意;
    • 方法访问权限:可以随意修改。

      多态的原理

      多态实现的必要条件:
  1. 继承;
  2. 重写;
  3. 父类引用指向子类实例(Father son = new Son())。

    Java 传参方式:传值

    除了基础数据类型,变量保存的是对象的引用。
    当函数参数为对象时,指向对象的变量(对象的引用) 原始变量引用被复制出了一个新变量并用于函数内部操作,这实际上就是传值;复制的变量与原始变量指向的对象是一样的,因此对参数变量的操作会改变原对象的内容,造成外部看来貌似是传引用,实则不是。

    基础语法

    final

  • 修饰字段:调用构造函数时进行初始化;
  • 修饰方法:子类不允许 @Override 该方法;
  • 修饰类:不允许被 extends,所有方法自动被 final 修饰,字段不一定。

    static

    与类相关,与对象无关。

  • 修饰字段:所有该类实例化的对象都共享该数据;

  • 修饰方法:不存在 this 指针,只能访问被 static修 饰的字段,可以被 extends 不能被 @Override
  • 修饰代码块:只在第一次new对象的时候执行一次。

    static 与继承

    ```java static class A { public static void test(A a) {

    1. System.out.println("A");

    }

    public static void test(B b) {

    1. 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); }

  1. 输出:
  2. ```bash
  3. A
  4. A

解释:

  1. 两个test()方法被重载;
  2. 用子类 B 去构造一个实例,并将其引用地址赋值给类型为 A 的 a。调用被 static 修饰的方法时,该方法不允许被重写,另外 A.test(a) 的输出根据参数类型 A 而非具体引用类型去匹配方法,因此输出为 “A”;
  3. 用子类 B 继承了父类 A 的 static 方法,方法不允许被重写,同样根据参数类型判断方法,因此输出为 “A”。

    static 对类型推断的影响

  4. 一般情况下,实例调用哪一个方法,根据实例自身是哪一个对象的引用,不需要看实例属于哪个类;

  5. 当方法被 static 修饰后,这个方法与实例无关,无法通过引用判断出需要调用哪个具体方法,因此被 static 修饰的方法不能被覆写,只能被继承。

    代码块

    {} 代码块

    加载类时,在构造函数之前执行,按定义的先后顺序执行,每次 new 一个实例就执行一次。

    static {} 代码块

    加载类时,在构造函数之前执行,按定义的先后顺序执行,只在第一次 new 实例时执行一次。

    代码块执行顺序

    不管有没有后续操作,加载了类就执行类中定义的静态代码块

  6. new 实例的顺序:静态代码块 -> 非静态代码块 -> 构造函数;

  7. 不 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 的类重写 equalshashCode 方法

  • 重写了 equals 但没重写 hashCode

两值个相等的对象可能算出来的哈希值不同,因此被安放在了两个不同的桶中,造成 key 值相等的对象存在两个的问题。

  • 重写了 hashCode 但没重写 equals

equals 比较两个对象的地址,只要不是同一个对象必为 false,因此值相等的两个 key 发生哈希碰撞后不会覆盖,而是存在两个值相等的 key。

HashCode 的生成

jvm 使用不同的参数 -XX:hashCode=<0~5> 选择不同的 HashCode 生成算法:

  1. 直接返回随机数(高并发时会自旋等待)。
  2. 对象的内存地址位移 + 异或。
  3. 固定返回 1(用于测试)。
  4. 随着对象个数自增。
  5. 直接返回对象地址。
  6. 当前状态进行异或(默认)。

哈希冲突

HashMapEntry 组成的数组(桶)构成,当 Entry 的 HashCode 冲突时,同一个 HashCode 下再用链表构成;链表的元素插入方式是头插;链表长度大于 8 时,转化为红黑树保存数据;
链表自动扩展时长度为 2 的整数次幂(为了 hash 后减少碰撞);
loadFactor 为 0.75(泊松分布,碰撞最小的扩展方式,时间和空间的折衷)。

使用红黑树的原因

普通的二叉排序树,在最坏情况下可能退化为线性结构;
平衡二叉树(AVL 树),追求完全平衡,插入节点产生旋转的时间复杂度是Java 基础 - 图1,但删除节点产生旋转的时间复杂度为Java 基础 - 图2
红黑树,不追求完全平衡,任何不平衡在三次旋转内可以解决,时间复杂度为Java 基础 - 图3
AVL 查找效率高,但红黑树则是查找、插入、删除效率上的折衷。

并发编程

线程

线程创建与使用

方法 描述 代码
继承 Thread 对不需要继承的类使用,start 方法是一个 native 方法,在 OS 中启动一个新线程后执行实现的 run 方法 ```java

public class ClassThread extends Thread { public void run() { // 线程具体任务 }

  1. public static void main(String[] args) {
  2. ClassThread ct = new ClassThread();
  3. // start 方法启动线程
  4. ct.start();
  5. }

}

  1. |
  2. | 实现 `Runnable` 接口 | 对需要继承的类使用,直接实现 `run` 方法,再通过该类的实例去创建一个 `Thread` 对象,调用该对象的 `start` 方法 | ```java
  3. public class ClassThread extends SuperClass implements Runnable {
  4. public void run() {
  5. // 线程具体任务
  6. }
  7. public static void main(String[] args) {
  8. ClassThread ct = new ClassThread();
  9. // 使用已有实例创建线程对象
  10. Thread t = new Thread(ct);
  11. // start 方法启动线程
  12. t.start();
  13. }
  14. }

| | 基于线程池 | 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() { // 线程具体任务 } }); } }

  1. |
  2. | `ExecutorService` `Callable<Class>` | 在类中实现 `Callable` 接口,在 `call` 方法中定义线程具体任务,再通过线程池提交任务的方式将执行结果保存在 `List<Future>` | ```java
  3. public class ClassCallable implements Callable<String> {
  4. private String data;
  5. public ClassCallable(String data) {
  6. this.data = data;
  7. }
  8. @Override
  9. public String call() throws Exception {
  10. // 线程具体任务并返回结果
  11. return data;
  12. }
  13. public static void main(String[] args) {
  14. // 创建线程池
  15. ExecutorService pool = Executors.newFixedThreadPool(5);
  16. // 保存结果的 List
  17. List<Future> results = new ArrayList<>();
  18. for(int i = 0; i < 5; i++) {
  19. // 创建带返回值的线程实例
  20. Callable c = new ClassCallable(i + "");
  21. // 保存线程池执行结果
  22. Future f = pool.submit(c);
  23. results.add(f);
  24. }
  25. // 关闭线程池
  26. pool.shutdown();
  27. }
  28. }

|

线程状态

image.png

线程可用方法

image.png

  • 等待 wait:释放锁,进入 WAITING 状态,需要等待唤醒(notify)或中断(interrupt)才有返回。
  • 睡眠 sleep:不释放锁,进入 TIMED_WAITING 状态,需要等待固定时长。
  • 放弃 yield:让出 CPU 并重新竞争时间片。
  • 中断 interrupt:只改变标识位,不会改变线程状态,Running 状态不会终止/TIMED_WAITING 状态提前结束。
  • 加入 join:阻塞一个线程到被 join 的线程结束,再转为Runnable 状态重新竞争时间片。
  • 唤醒 notify:唤醒在某个对象上等待的线程(任选其一,如果全部线程都要唤醒则使用 notifyAll 方法),一般在 wait 方法后使用。
  • 护线程 setDaemon:定义一个守护线程。

    守护线程(服务线程):

    • 为用户线程提供公共服务,没有用户线程时自动离开,如垃圾回收线程;
    • 周期性地执行某些任务或处理事件;
    • 与 JVM 同生共死(因此所有线程都是守护线程时 JVM 就可以退出了)

方法对比

sleepwait
  • Thread.sleep()Object.wait() 的关系;
  • sleep 不释放锁,wait 释放锁;
  • sleep 肯定会恢复运行状态,wait 不被 notify 不会进入运行状态。

    startrun
  • start 启动线程,进入 Runnable 状态但不是 Running 状态;

  • run 方法包含线程任务逻辑,只有调用了该方法才进入 Running 状态。

    无锁情况下 notify

    wait/notify/notifyAll必须保证调用的对象是同步的(处于 synchroized(object) 语句块内部),否则会报错。
    ⚠️ 需要先 notifywait,原因:被 wait 后的线程不再是同步的,不持有锁了。

  • wait:阻塞当前线程,释放所有锁,使用前提是已经获得了锁(进入同步状态);

  • notify:唤醒等待状态的线程,不会立即释放锁,因此尽量调用后退出临界区,让其他线程获得锁;
  • notifyAll:唤醒全部等待状态的线程。

    线程池

    image.png

目的

  • 利用多线程压榨 CPU 算力;
  • 降低创建、销毁线程过程的 CPU 开销与 GC 压力;
  • 提高任务响应速度,无需等待线程创建;
  • 限制线程数量并可以进行统一的资源分配、调优和监控。

    流程

    Java 基础 - 图7

    拒绝策略

  1. AbortPolicy:直接丢弃,抛出 RejectedExecutionException 异常;
  2. DiscardPolicy:直接丢弃;
  3. DiscardOldestPolicy:丢弃等待队列中最老的未处理任务,将被拒绝的任务添加到等待队列;
  4. CallerRunsPolicy:不进入线程池,由调用者线程执行任务。

    锁分类

    image.png

    乐观锁与悲观锁

  • 乐观锁通过 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)实现,在操作数据前就加锁让其他需要该数据的线程阻塞。会先尝试乐观锁,若获取不到进入悲观锁。具体实现如:synchronizedLock 具体实现等。

    自旋锁

    image.png
    自旋锁为了减少 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 的同步问题类似)。
    • 工作原理:每个处理器(线程)监听总线上传播的数据(主内存),检查自己缓存(本地内存)的值是否已经过期;如果监听到了修改,会重新将数据存储到缓存(本地内存)中。