前言
最近在B站观看了马士兵老师关于Java多线程的精彩分享,我将核心知识点进行了系统梳理,整理成这份较为完整的多线程学习笔记。点击下图,即可一览本次讨论的全部内容概览。

正文
多线程基础
1. synchronized 锁定的代码块范围越小,效率自然越高——毕竟线程等待锁的时间会显著缩短。
2. synchronized 锁的是堆内存中的对象实例,而非对象的引用。若用于普通方法,锁的是 this,即当前对象在堆上的地址;若用于静态方法或静态变量,锁的是 Class 对象。一个常见的陷阱是:如果锁定的堆内存被重新赋值指向了新对象,原本使用同一引用的线程将不再受锁的约束。
3. volatile 保证变量在多线程间的可见性,但无法解决原子性问题,因此不能替代 synchronized。若需要原子性操作,可使用 AtomicInteger 等原子类,但其功能不如 synchronized 全面。
4. 切忌使用字符串常量作为锁对象。若两个字符串值相同(例如都是 "hello"),一个线程先锁住变量 a,另一个线程再去锁变量 b,极易引发死锁。这是因为 JVM 常量池会缓存字符串,导致两个引用指向同一对象。
5. wait() 会释放锁,而 notify() 不会——这两个方法是线程间通信的核心机制。使用它们时需要格外谨慎,否则容易导致线程逻辑混乱。
6. CountDownLatch 如同一个门闩:某个线程调用 await() 等待,直到其他线程将计数器减至 0。这与 TypeScript 中的 forkJoin 类似,专门用于等待多个线程完成状态变更后,主线程再继续执行。
7. 写操作加锁而读操作不加锁,大概率会出现脏读。常见的解决方案是 CopyOnWrite 机制——读操作不加锁,写操作时复制一份副本进行修改,保证读到的数据始终一致。
ReentrantLock 重入锁
1. ReentrantLock 可以替代 synchronized,但属于手工锁——必须手动释放,否则线程会一直阻塞。
2. 它和 synchronized 最大的不同在于支持 tryLock:尝试获取锁,获取成功则执行某任务,失败则执行另一任务,而非像 synchronized 那样盲目等待。
3. 通过构造函数的参数 true,可以创建“公平锁”。假设 5 个线程同时等待同一把锁释放,默认是非公平的——谁先抢到算谁的;而公平锁则会按等待时间排序,让等待最久的线程优先获得锁。
4. wait() 通常应与 while 循环配合使用,而非 if。因为 wait() 会释放锁,被唤醒时可能有多个线程同时被唤醒,若仅用 if 判断条件,会出现竞态问题。
5. 需要通知其他线程苏醒时,应使用 notifyAll()。若使用 ReentrantLock,还可借助 Condition 实现精准唤醒:通过 newCondition() 获取 Condition 对象,不同条件可创建不同的 Condition,从而实现更细粒度的线程唤醒控制。
6. ReentrantLock 与传统 Thread 方法的对应关系如下:
| 等待方法 | 通知方法 | 通知全部 |
|---|---|---|
| condition.await() | condition.signal() | condition.signalAll() |
| wait() | notify() | notifyAll() |
7. ThreadLocal 为每个线程提供独立的存储空间——这是典型的以空间换时间的策略。
并发容器
1. 若要确保 List 的 size() 和 remove() 操作具备原子性(例如购票系统),推荐使用 ConcurrentLinkedQueue。它的 poll() 方法先删除元素再返回,若返回 null 表示队列已空——这种“先删后验”的设计十分巧妙。
2. ConcurrentHashMap 取代了 HashTable:后者使用一个大锁锁住整个对象,而前者采用更细粒度的锁机制,每次只锁定一个 segment。更详细的设计可参考这篇文档 https://www.cnblogs.com/heyonggang/p/9112731.html。
3. 在高并发场景下,若 Map 还需要排序功能,可用 ConcurrentSkipListMap 替代 TreeMap。它的插入效率相对较低,但查询速度很快——适合读多写少的场景。
4. CopyOnWriteList 采用写时复制策略:写入数据时复制一份新列表,在新列表上添加元素,最后将引用指向新列表。这种方式下,读操作完全无需加锁,因此读效率高、写效率低。
5. Collections.synchronizedXXX() 可以将未加锁的容器包装成线程安全版本,支持 List、Set、Map 等类型。
6. 高并发环境下常用的两种队列:
- ConcurrentLinkedQueue
- BlockingQueue(LinkedBlockingQueue 是无界队列,ArrayBlockingQueue 是有界队列,DelayQueue 用于定时任务)
队列的三个方法需注意区别:
add():队列满时直接抛出异常offer():队列满时返回false,不会报错put():队列满时阻塞等待,直到有空间释放
7. DelayQueue 中的 take() 方法,若没有消费者取走元素,会一直阻塞等待。
8. SynchronousQueue 容量为 0,不能使用 add(),只能通过 put() 放入一个元素,然后阻塞等待消费者取走。
线程池
1. Executor 是线程池的顶级接口,仅包含一个 execute() 方法,接收 Runnable 参数。
2. ExecutorService 扩展了 Executor,可提交 Runnable 或 Callable 任务,提供了 execute() 和 submit() 两种方法。
3. Callable 与 Runnable 的最大区别在于:call() 方法有返回值,且能够抛出异常。
4. Executors 是一个工具类,提供了五种初始化线程池的方式:
newFixedThreadPool:固定线程数量的线程池newCachedThreadPool:缓存线程池,默认最大线程数为Integer.MAX_VALUE,线程空闲 60 秒后自动销毁newSingleThreadExecutor:单例线程池,保证任务按 FIFO 顺序执行newScheduledThreadPool:支持定时任务的线程池newWorkStealingPool:工作窃取线程池,每个线程拥有独立的任务队列,空闲线程会“窃取”其他线程的任务来执行
5. ThreadPool 的核心思想:保持固定数量的工作线程,空闲时不创建新线程,从而减少资源开销。
6. Future 作为 Callable 的返回值,可用于获取异步计算的结果。
7. 以下是演示代码——分别使用单线程和多线程计算指定范围内的所有质数,对比两者性能差异:
package msb_013;
import ja va.util.ArrayList;
import ja va.util.List;
import ja va.util.concurrent.Callable;
import ja va.util.concurrent.ExecutionException;
import ja va.util.concurrent.ExecutorService;
import ja va.util.concurrent.Executors;
import ja va.util.concurrent.Future;
/**
* 计算指定范围内所有的质数
*/
public class Test03_ParallelComputing {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 单线程计算,耗时约12秒
long start = System.currentTimeMillis();
List list = getPrime(1, 500000);
long end = System.currentTimeMillis();
System.out.println(end - start);
// 使用固定线程池(6个线程)并行计算,耗时缩短到约6秒
ExecutorService service = Executors.newFixedThreadPool(6);
MyTask taskA = new MyTask(1, 200000);
MyTask taskB = new MyTask(200001, 350000);
MyTask taskC = new MyTask(350001, 450000);
MyTask taskD = new MyTask(450001, 500000);
Future> futureA = service.submit(taskA);
Future> futureB = service.submit(taskB);
Future> futureC = service.submit(taskC);
Future> futureD = service.submit(taskD);
start = System.currentTimeMillis();
List listA = futureA.get();
List listB = futureB.get();
List listC = futureC.get();
List listD = futureD.get();
end = System.currentTimeMillis();
System.out.println(end - start);
service.shutdown();
}
static class MyTask implements Callable> {
int start, end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public List call() throws Exception {
return getPrime(start, end);
}
}
public static boolean isPrime(int number) {
for (int i = 2; i < number / 2; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
public static List getPrime(int start, int end) {
List list = new ArrayList<>();
for (int i = start; i <= end; i++) {
if (isPrime(i)) {
list.add(i);
}
}
return list;
}
}
后记
目前仅为初步的知识点整理,后续会持续更新,加入更深入的理解与实践经验,不断完善这份多线程学习笔记。
