Java多线程JUC实现
| 上一个章节 [Java多线程理论基础-JUC | Bulalalla’s Blog](https://bulalalla.github.io/posts/Java多线程理论基础-JUC/) 介绍了一些多线程的理论概念,本章节将从JUC实现的角度介绍如何进行多线程编程: |
- Thread,介绍Java中实现多线程的方式。
- ThreadLocal,介绍线程进行变量本地访问时的实现方式。
- ThreadExecutor,介绍线程池的作用,实现,使用方法。
- 常见的并发容器,Array,Map…
- CompletableFuture,接收线程运行结果
Thread实现方式
目前Java实现多线程有以下几种方式:
- 继承Thread类,并重写run方法
- 实现Runable接口,然后将实例传递给Thread对象
- 实现Callable接口
- 使用JUC的一套工具:JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。
线程的六个状态
- NEW:新建状态
- RUNABLE:线程已获取运行所需的资源,进入就绪状态
- BLOCKED:线程未获取/失去运行所需的资源,进入阻塞状态
- WAITING:无限期等待状态(如调用
Object.wait()) - TIMED_WAITING(Java特有):有限期等待状态(如调用
Thread.sleep()) - TERMINATED:终止状态,线程因异常终止或执行结束
1
2
3
4
5
新建(New) → 就绪(Runnable) → 运行(Running) → 终止(Terminated)
↑ ↓
└── 阻塞(Blocked)
唤醒和挂起为动作
睡眠为状态,挂起一个线程,通常指调用方法使其进入阻塞状态
1. 继承Thread类
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread {
public void run() {
// ...
}
}
...
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
2. 实现Runnable接口
实现一个Runnable接口
1 2 3 4 5
public class MyRunnable implements Runnable { public void run() { // ... } }
创建实例,并将其传入Thread对象,仍然以
Thread.start()调用执行1 2 3 4 5
public static void main(String[] args) { MyRunnable instance = new MyRunnable(); Thread thread = new Thread(instance); thread.start(); }
3. 实现Callable接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
1
2
3
4
5
6
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
1
2
3
4
5
6
7
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
4. 使用JUC工具
JUC工具提供了不止创建线程的方法,还有各种类型的锁,并发集合等。
ThreadLocal详解
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2);
});
thread1.start();
thread2.start();
}
}
本质上,变量还是存储在了各个Thread中,每个Thread对象有一个ThreadLocalMap实例,这个Map的Key为ThreadLocal,value为该线程在ThreadLocal中存储的值,因此,一个ThreadLocal对象只能在一个线程中,存储一个value。
1. 为什么ThreadLocalMap的Key必须是ThreadLocal对象?
Java中ThreadLocalMap的Key设计为ThreadLocal类型,主要基于以下原因:
- 唯一性保证
ThreadLocal实例的唯一性:每个ThreadLocal对象独立标识一个线程本地变量,作为Key可确保不同变量间无冲突。例如,两个不同的ThreadLocal实例作为Key,即使变量名相同,也能在同一个线程中区分不同的值。- 避免命名冲突:若用其他类型(如
String),需手动管理唯一性,而ThreadLocal实例的天然唯一性简化了设计。
- 哈希优化
- 内部哈希码机制:
ThreadLocal内部维护threadLocalHashCode,在实例化时通过静态原子计数器生成,确保哈希分布均匀。ThreadLocalMap使用该哈希码计算存储位置,优化了查找效率(开放地址法处理冲突)。 - 性能优势:专用哈希机制减少冲突,提升
get/set操作的性能。
- 内部哈希码机制:
内存泄漏防护
- 弱引用(WeakReference)的Key:当
ThreadLocal实例失去强引用时,Key会被自动回收(Entry的Key变为null),ThreadLocalMap在后续操作(如set、remove)中清理无效Entry。若Key为其他类型(如强引用的String),可能导致Entry无法回收,引发内存泄漏。
- 弱引用(WeakReference)的Key:当
- 类型安全与封装
- 类型约束:通过泛型限定Key为
ThreadLocal,确保只有合法变量可访问,避免类型错误。 - 封装性:
ThreadLocalMap是内部实现细节,不对外暴露。固定Key类型防止开发者误用,维护代码清晰性。
- 类型约束:通过泛型限定Key为
能否使用其他类型作为Key?
- 技术上可行但设计上不可取:
- 若强行修改源码替换Key类型(如改为
Object),需重新设计哈希机制、内存管理和类型检查,可能破坏现有功能。 - 非
ThreadLocal类型无法利用弱引用自动清理机制,易导致内存泄漏。 - 失去唯一性保障,需额外逻辑避免冲突,增加复杂度。
- 若强行修改源码替换Key类型(如改为
示例说明
假设用String作为Key:
1
2
3
ThreadLocalMap<String> map = new ThreadLocalMap<>();
map.set("userSession", session1); // 线程A
map.set("userSession", session2); // 线程B
多个线程或不同上下文中使用相同字符串Key会导致值覆盖。而使用ThreadLocal实例:
1
2
ThreadLocal<String> userSession = new ThreadLocal<>();
userSession.set(session1); // 每个线程独立存储,互不干扰
每个ThreadLocal实例作为Key,天然隔离不同变量。
结论
ThreadLocalMap的Key必须是ThreadLocal类型,这是Java为实现线程隔离、高效哈希管理和内存安全所做的核心设计。改用其他类型会破坏唯一性、弱引用机制和类型安全,导致功能异常或资源泄漏。
2. 为什么ThreadLocal能存储任意类型变量?
答:实际上,ThreadLocal并不存储变量的值,这个值存储在Thread对象的ThreadLocalMap对象中,这个map对象维护了一个Entry数组,值其实存储在了数组中,所以value其实被存到了Entry对象中。可以看Entry对象的定义:
1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这个值最终被转换为Object对象了,所以它可以存储任意类型的值。

