
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
文章发布较早,内容可能过时,阅读注意甄别。
在 Java 中,接口和抽象类都是实现抽象的机制,但它们在设计动机、功能和使用方式上存在显著差异。
设计动机和理念:
接口的设计是自顶向下的:
Runnable)的对象”、“我需要一个能比较(Comparable)的对象”、“我需要一个能连接数据库(Connection)的对象”。我们先定义这个“能做什么”的规范,再去考虑“谁来实现”以及“怎么实现”。抽象类的设计是自底向上的:
Circle, Rectangle, Triangle),它们都有计算面积的方法,也有共同的颜色属性。我可以把这些共同的东西抽象成一个 AbstractShape 类。”总结自顶向下与自底向上:
其他主要区别:
除了设计理念上的差异,还有以下几个关键的技术区别:
方法实现:
public abstract,不允许有方法实现。default 方法(有默认实现)和 static 方法(静态实现)。private 方法和 private static 方法。abstract 方法(没有实现,必须由子类实现)和具体方法(有实现,子类可以直接继承或覆盖)。构造函数和成员变量:
public static final,即常量。private, protected, public),并且可以不是常量。多重继承:
总结对比表:
| 特性 | 接口 (Interface) | 抽象类 (Abstract Class) |
|---|---|---|
| 设计理念 | 自顶向下:先定义行为规范/契约 | 自底向上:先有具体类,再抽取共性作为模板 |
| 定义关键字 | interface | abstract class |
| 方法实现 | Java 8 前无实现;Java 8+ 可有 default/static 方法 | 可有抽象方法 (无实现) 和具体方法 (有实现) |
| 构造器 | 不能有 | 可以有 |
| 成员变量 | 默认 public static final (常量) | 可有各种修饰符 (private, protected, public),可变变量 |
| 多重继承 | 一个类可实现多个接口 | 一个类只能继承一个抽象类 |
| 强调 | 行为、能力、契约、多态 | 模板、父子关系、代码复用 |
| 实例化 | 不能直接实例化 | 不能直接实例化 (但有构造器供子类调用) |
总结:JDK 代理是面向接口的代理,CGLIB 代理是面向类的代理(通过继承)。
反射机制允许程序在运行时检查或修改类的结构、方法和属性,而无需在编译时就知道这些信息。
具体应用举例:
如何应用反射:
要应用反射,通常涉及以下核心 API:
Class.forName("com.example.MyClass") 或 myObject.getClass() 或 MyClass.class。Class.getConstructor() / getConstructors(),然后 Constructor.newInstance()。Class.getMethod("methodName", paramTypes...) / getMethods(),然后 Method.invoke(object, args...)。Class.getField("fieldName") / getFields(),然后 Field.get(object) / Field.set(object, value)。想象一下你有一个巨大的衣柜,里面有很多抽屉,每个抽屉上都贴着一个标签(这个标签就是键,Key),你把衣服(这个衣服就是值,Value)放进对应的抽屉里。
HashMap 的原理也差不多:
标签变数字(哈希函数):当你给 HashMap 一个 Key(比如你要存一件红色的 T 恤,Key 就是“红色 T 恤”),它会先对这个 Key 进行一番计算,把这个 Key 变成一个数字(这个计算过程就叫做哈希函数,结果叫哈希值)。
5。找抽屉位置(索引):这个数字可能很大,但你的衣柜抽屉是有限的。所以 HashMap 会用这个数字再做一次运算(通常是取模运算),把它映射到衣柜里某个具体的抽屉位置上。
5 在你的 HashMap 里可能对应第 5 号抽屉。放衣服进去(存储):
Key-Value 对)。5 号抽屉,这就叫哈希碰撞),HashMap 不会直接覆盖掉。它会把新的衣服挂到这个抽屉里已经有的衣服的后面,形成一个链子。这个链子可以是链表(早期版本)或者红黑树(当链子太长时,为了提高查找效率,JDK8 及以后会转换)。 5 号抽屉里本来有“蓝色衬衫”,现在“红色 T 恤”也来了,它们就会排队,比如“蓝色衬衫” → “红色 T 恤”。找衣服(查找):当你想要找“红色 T 恤”的时候,HashMap 也会对“红色 T 恤”这个 Key 进行同样的计算,得到同样的抽屉位置(第 5 号)。然后它会到第 5 号抽屉里,沿着链子一条一条地比对,直到找到“红色 T 恤”为止。
核心思想就是:
Key 算出哈希值,再映射到数组索引,大大提高了查找效率,不用一个一个地遍历所有元素。Key 算出相同索引的情况。想象一下你有很多东西要管理,比如一堆照片、一份购物清单、一套学生花名册。Java 为了帮你管理这些东西,提供了各种“容器”,这些容器就是集合类。它们主要分成几大家族:
List (列表) 家族:
ArrayList:基于数组实现。查改快,增删慢(特别是中间位置)。就像一排整齐的格子,找东西方便,但中间加东西要挪动后面的所有格子。LinkedList:基于链表实现。增删快,查改慢。就像一串手拉手的人,加个人很容易插队,但找人要一个一个问过去。Set (集合) 家族:
HashSet:基于 HashMap 实现。查找、添加、删除都很快。它利用了哈希表的快速查找特性。LinkedHashSet:继承 HashSet,内部用链表维护插入顺序。既保证不重复,又能记住你添加的顺序。TreeSet:基于红黑树实现。能自动排序(自然顺序或自定义顺序)。Map (映射) 家族:
Key 唯一(就像抽屉标签不能重复),Value 可以重复。HashMap:基于哈希表实现。查找、添加、删除都很快(我们上面原理讲的就是它)。无序。LinkedHashMap:继承 HashMap,内部用链表维护插入顺序。既能快速查找,又能记住你添加的顺序。TreeMap:基于红黑树实现。能自动按 Key 排序。总结:
想象你的衣柜一开始只有 16 个抽屉。你不断往里放衣服,直到抽屉越来越满,或者因为哈希碰撞导致链子越来越长,找衣服越来越慢。
HashMap 也一样,当它达到一定程度时,为了保持效率,它会进行扩容:
扩容的触发条件:
HashMap 有一个“装载因子”,默认是 0.75。意思是当抽屉里衣服的数量(也就是 HashMap 里的元素个数)达到抽屉总数的 75% 时,就触发扩容。 16 个抽屉,当放了 16 * 0.75 = 12 件衣服时,就会开始考虑扩容。8),并且 HashMap 的总抽屉数量(容量)也达到了另一个阈值(默认是 64),那么这个链表会转换为红黑树,以提高查找效率。如果容量还没到 64,那么会先触发一次扩容。扩容操作的步骤:
HashMap 扩容时,通常会将抽屉的总数(底层数组的长度)翻倍。这是为了利用位运算来提高计算新索引的效率,因为 HashMap 的容量总是 2 的幂次方。 16 个抽屉变成 32 个抽屉。16 个抽屉的衣柜里对应第 5 号抽屉。现在变成 32 个抽屉的衣柜,它可能仍然在 5 号抽屉,也可能去了 5 + 16 = 21 号抽屉,这取决于它的哈希值的某一位。扩容的优缺点与优化建议:
HashMap 在元素增多的情况下依然能保持较高的查找、插入和删除效率(接近 O(1))。O(n),其中 n 是 HashMap 中元素的数量。HashMap 的大小,最好在创建时就指定一个合适的初始容量。这样可以减少不必要的扩容次数,从而提升性能。简单来说:HashMap 会观察自己有多满。当它觉得太挤了(元素数量达到容量的 75%),就会造一个更大的衣柜(通常是原来的两倍大)。然后,它会高效地把所有衣服都拿出来,重新计算它们在新衣柜里的位置(JDK 8 后更高效),再全部放进去。这样虽然会有一小段时间的开销,但确保了以后还能继续快速找衣服。
想象你是一个餐馆老板,生意很好,经常有客人来点餐。如果你每次有客人来都临时找一个厨师来做菜,做完就让他走,这样太麻烦了,而且招人、培训人都需要时间,效率很低。
线程池就相当于你的这个餐馆的“专属厨师团队”:
线程池的优点:
核心组件:
ThreadPoolExecutor:Java 中实现线程池的类。corePoolSize):即使没有任务也会保留的最小线程数。maximumPoolSize):允许创建的最大线程数。BlockingQueue):存放待执行任务的队列。keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。RejectedExecutionHandler):当线程池和任务队列都满时,如何处理新提交的任务。在多线程编程中,为了让多个线程和谐、高效地工作,Java 提供了很多“工具”,可以帮助我们管理和协调线程。我主要使用过以下这些:
ConcurrentHashMap (并发哈希表):
作用:是一个线程安全且高效的哈希表,支持并发访问。解决了传统 HashMap 在多线程环境下不安全的问题,同时性能比 Hashtable 等加锁的 Map 更好。
用法:多个线程可以同时对它进行读写操作,而不会导致线程安全问题。
类比:就像一个有多个收银台的超市,不同收银员(线程)可以同时处理顾客(操作数据),互不干扰,大大提高了效率。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1); // 存入 key1-1
System.out.println("存入 key1 -> " + map.get("key1")); // 读取 key1
// 如果 key2 不存在,则存入 k -> 2 的结果 (即 2)
map.computeIfAbsent("key2", k -> {
System.out.println("'key2' 不存在,计算并存入 2");
return 2;
});
System.out.println("第一次 computeIfAbsent 后 key2 的值:" + map.get("key2"));
// 再次使用 computeIfAbsent。
// 因为 key2 已经存在,k -> 3 的逻辑不会执行,map 中的值也不会改变
map.computeIfAbsent("key2", k -> {
System.out.println("'key2' 已经存在,k -> 3 的逻辑不会执行");
return 3; // 此值不会被存入
});
System.out.println("第二次 computeIfAbsent 后 key2 的值:" + map.get("key2"));AtomicInteger (原子整数):
作用:提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较并设置。避免了使用 synchronized 关键字带来的性能开销。
用法:适用于需要频繁对数值进行无锁操作的场景。
类比:就像一个显示实时人数的计数器,多个人同时点击“进入”或“离开”,计数器也能准确、安全地更新,不需要排队等待。
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 线程安全地递增
atomicInt.decrementAndGet(); // 线程安全地递减
atomicInt.compareAndSet(1, 2); // 比较并设置 (CAS 操作)Semaphore (信号量):
作用:控制访问资源的线程数量,可以用来实现限流或访问控制。
用法:在资源有限的情况下,控制同时访问的线程数量。
类比:就像一个有固定车位的停车场。每次有车(线程)想进入,就得先拿一个停车许可。停车位满了,后面的车就得等着。
Semaphore semaphore = new Semaphore(3); // 最多允许 3 个线程同时访问
try {
semaphore.acquire(); // 获取许可,如果许可不够则阻塞
// 执行任务(例如访问数据库连接、文件等有限资源)
} finally {
semaphore.release(); // 释放许可
}CyclicBarrier (循环屏障):
作用:让一组线程到达一个共同的同步点,然后一起继续执行。常用于分阶段任务执行。
用法:适用于需要所有线程在某个点都完成再继续的场景。
类比:就像一次团队协作的闯关游戏。设定 3 人一组,只有当所有 3 名队员都到达当前关卡的集合点时,大家才能一起通过,进入下一关。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程都到达了屏障点,开始下一阶段任务");
});
Runnable task = () -> {
try {
// 执行任务的第一阶段
System.out.println(Thread.currentThread().getName() + "完成第一阶段任务");
barrier.await(); // 等待其他线程到达屏障
// 执行任务的第二阶段
System.out.println(Thread.currentThread().getName() + "完成第二阶段任务");
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task, "线程 A").start();
new Thread(task, "线程 B").start();
new Thread(task, "线程 C").start();CountDownLatch (倒计时门闩):
作用:一个线程(或多个)等待其他线程完成操作。
用法:适用于主线程需要等待多个子线程完成任务的场景。
类比:就像一个项目的验收。项目经理(主线程)需要等待 3 个小组(子线程)各自完成他们的模块开发(countDown()),当所有模块都完成时,项目经理才能宣布项目完成(await())。
CountDownLatch latch = new CountDownLatch(3); // 设置计数器为 3
Runnable task = () -> {
try {
// 执行任务
System.out.println(Thread.currentThread().getName() + "完成任务");
} finally {
latch.countDown(); // 任务完成,计数器减一
}
};
new Thread(task, "子线程 1").start();
new Thread(task, "子线程 2").start();
new Thread(task, "子线程 3").start();
latch.await(); // 主线程等待所有任务完成,计数器归零时才继续
System.out.println("所有任务都完成了,主线程继续执行");BlockingQueue (阻塞队列):
作用:是一个线程安全的队列,支持阻塞操作。适用于生产者 - 消费者模式。
用法:生产者线程将元素放入队列,队列为满时生产者线程阻塞;消费者线程从队列中取出元素,队列为空时消费者线程阻塞。
类比:就像一个有限容量的生产线传送带。生产工人(生产者线程)往上传送产品,如果传送带满了,他就得等等;质检工人(消费者线程)从传送带上取产品,如果传送带空了,他也得等等。
BlockingQueue<String> queue = new LinkedBlockingQueue<>(2); // 容量为 2 的阻塞队列
// 生产者线程
Runnable producer = () -> {
try {
queue.put("item1"); // 放入元素,如果队列满则阻塞
System.out.println("生产者放入 item1");
queue.put("item2");
System.out.println("生产者放入 item2");
queue.put("item3"); // 队列已满,生产者将在此阻塞,直到有消费者取出元素
System.out.println("生产者放入 item3");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 消费者线程
Runnable consumer = () -> {
try {
Thread.sleep(1000); // 模拟消费者处理需要时间
String item = queue.take(); // 取出元素,如果队列空则阻塞
System.out.println("消费者取出 " + item);
item = queue.take();
System.out.println("消费者取出 " + item);
item = queue.take();
System.out.println("消费者取出 " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(producer, "生产者").start();
new Thread(consumer, "消费者").start();CAS 是一种非常巧妙且高效的并发操作,它避免了传统锁机制(比如 synchronized)带来的性能开销。
CAS 的核心思想:
它包含三个操作数:
操作过程:
X 位置的值从 旧值1 改成 新值2。”X 位置的值是不是真的还是 旧值1。 旧值1:说明在线程 A 检查到它并打算修改的这段时间里,没有其他线程动过它,那么 CAS 操作就成功,把 X 位置的值改成 新值2。旧值1:说明有其他线程在我不知情的情况下,已经把 X 位置的值改掉了。CAS 操作就失败,不进行任何修改。类比:
想象你正在买一件限量版 T 恤,只剩最后一件了。
1 表示有货,0 表示没货。1)。0)。你拿着钱去柜台:
1),我就把它买走,变成没货(改成 0)。”1):那么你买成功了,T 恤变成没货(改成 0),交易完成。0):说明在你来之前,别人已经买走了。你的交易失败。CAS 的优点:
CAS 的缺点:
A 变成 B,又从 B 变回 A,那么 CAS 会认为没有发生变化,但实际上已经变过了。这个问题可以通过引入版本号(AtomicStampedReference)来解决。AtomicReference。Java 中的 java.util.concurrent.atomic 包下的类(如 AtomicInteger、AtomicLong)就是基于 CAS 实现的,它们提供了一些基本类型的原子操作。
简单来说,AQS(AbstractQueuedSynchronizer,抽象队列同步器)起到了一个抽象、封装的作用。
它提供了一套通用的机制:将线程排队、入队、加锁、中断、唤醒等通用的同步操作抽象出来,并封装在一个框架里。这使得 JUC 包中各种复杂的同步组件(如 ReentrantLock、CountDownLatch、Semaphore 等)能够基于这套机制来实现,避免了重复造轮子,大大简化了并发工具的开发。
核心构成:它主要通过维护两个关键部分来管理线程对共享资源的访问:
state 变量):这个 state 变量用 volatile 修饰,表示当前资源的同步状态。 ReentrantLock 的独占模式)中,state 为 0 表示资源未被占用,为 1 表示资源已被占用。Semaphore)中,state 可能表示剩余的许可数量。如何工作:
state。如果 state 允许获取(比如锁未被占用),线程就成功获取资源。如果 state 不允许获取,线程就会被封装成一个节点,加入到等待队列的尾部,并被阻塞挂起。state 的值。如果修改后 state 允许其他线程获取资源,并且等待队列中有线程,AQS 就会唤醒队列头部的线程去尝试获取资源。定制性:AQS 是一个抽象类,它定义了获取和释放资源的骨架方法(如 acquire()、release()),但具体的“如何判断资源是否能获取”、“如何修改 state”等逻辑,需要具体的实现类(如 ReentrantLock 的内部类 Sync)来自己控制和实现。比如,独占锁和共享锁对 state 的操作逻辑就不同。
AQS 常见的实现类有:ReentrantLock、CountDownLatch、Semaphore 等等。
共同点:
它们都是可重入锁,这意味着持有锁的线程可以再次获取该锁而不会发生死锁。
核心区别:
lock() 和 unlock() 方法),这就要求开发者必须在 finally 块中确保 unlock() 被调用,以避免死锁。volatile 关键字的主要作用是保证变量的可见性和禁止指令重排优化。
可见性 (Visibility):
问题背景:在多线程编程中,为了提高性能,每个线程通常会有自己的工作内存(或称为本地缓存),用于存储变量的副本。当一个线程修改了一个变量的值时,这个修改可能只会先反映在它的工作内存中,而不会立即同步到主内存。这就导致了另一个线程在读取同一个变量时,可能会从自己的旧的本地缓存中读取到过期的值,而不是主内存中的最新值。这种现象被称为“缓存一致性问题”,或者更通俗地说,“看见旧值”的现象。
volatile 的作用:
volatile 关键字确保变量的可见性。当一个线程修改了 volatile 变量的值,这个新值会立即被刷新到主内存中。volatile 变量时,它们会强制性地从主内存中重新获取最新值,而不是使用自己本地缓存中的旧值。volatile 变量的读写操作总是能看到最新状态。禁止指令重排序 (Ordering):
问题背景:为了提高程序执行效率,编译器和处理器可能会对指令进行优化,调整其执行顺序,这种优化被称为“指令重排序”。在单线程环境下,这种重排序不会改变程序的最终结果(遵循“as-if-serial”语义)。然而,在多线程环境下,如果没有正确的同步机制,指令重排序可能会导致意想不到的错误结果。
volatile 的作用:
volatile 变量还通过插入内存屏障(Memory Barrier)来禁止特定情况下的指令重排序,从而保证程序的执行顺序符合预期。volatile 变量的写操作会在其前面插入一个 StoreStore 屏障,在其后面插入一个 StoreLoad 屏障。这确保了在 volatile 写操作之前的代码执行结果,对所有线程都是可见的,并且 volatile 写操作本身必须在后续的任何读操作之前完成。volatile 变量的读操作会在其后面插入一个 LoadLoad 屏障和 LoadStore 屏障。这确保了 volatile 读操作能够读取到最新的值,并且在 volatile 读操作之后的代码,不会被重排序到 volatile 读操作之前。重要补充:volatile 不保证原子性
虽然 volatile 解决了可见性和部分有序性问题,但它不保证操作的原子性。
volatile int count = 0; count++; 这个操作看起来很简单,但它实际包含了三个步骤: count 的当前值。count 进行加 1 操作。count。count++,volatile 只能保证每次读写 count 时,都是从主内存获取最新值并及时刷新到主内存。但是,它不能保证从读取到写入这三个步骤作为一个整体是不可分割的。在一个线程读取 count 后,另一个线程可能在它写入之前也读取了 count 的旧值,导致最终的 count 值不正确。java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)或使用 synchronized 或 ReentrantLock。总结:
volatile 适用于:当一个变量被多个线程共享,但其读写操作是独立的,不依赖于变量的当前值,或者读操作远多于写操作,并且写操作不依赖于读操作的结果(即只需要保证可见性和有序性,不需要原子性)的场景。例如,一个用于控制线程停止的 boolean 标记。
评论