文章

Java多线程JUC实现

上一个章节 [Java多线程理论基础-JUCBulalalla’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)
唤醒和挂起为动作
睡眠为状态,挂起一个线程,通常指调用方法使其进入阻塞状态

image-20250225202414865

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接口

  1. 实现一个Runnable接口

    1
    2
    3
    4
    5
    
    public class MyRunnable implements Runnable {
        public void run() {
            // ...
        }
    }
    
  2. 创建实例,并将其传入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();
    }
}

image-20250225202255270

本质上,变量还是存储在了各个Thread中,每个Thread对象有一个ThreadLocalMap实例,这个Map的Key为ThreadLocal,value为该线程在ThreadLocal中存储的值,因此,一个ThreadLocal对象只能在一个线程中,存储一个value

1. 为什么ThreadLocalMap的Key必须是ThreadLocal对象?

Java中ThreadLocalMap的Key设计为ThreadLocal类型,主要基于以下原因:

  1. 唯一性保证
    • ThreadLocal实例的唯一性:每个ThreadLocal对象独立标识一个线程本地变量,作为Key可确保不同变量间无冲突。例如,两个不同的ThreadLocal实例作为Key,即使变量名相同,也能在同一个线程中区分不同的值。
    • 避免命名冲突:若用其他类型(如String),需手动管理唯一性,而ThreadLocal实例的天然唯一性简化了设计。
  2. 哈希优化
    • 内部哈希码机制ThreadLocal内部维护threadLocalHashCode,在实例化时通过静态原子计数器生成,确保哈希分布均匀。ThreadLocalMap使用该哈希码计算存储位置,优化了查找效率(开放地址法处理冲突)。
    • 性能优势:专用哈希机制减少冲突,提升get/set操作的性能。
  3. 内存泄漏防护

    • 弱引用(WeakReference)的Key:当ThreadLocal实例失去强引用时,Key会被自动回收(Entry的Key变为null),ThreadLocalMap在后续操作(如setremove)中清理无效Entry。若Key为其他类型(如强引用的String),可能导致Entry无法回收,引发内存泄漏。
  4. 类型安全与封装
    • 类型约束:通过泛型限定Key为ThreadLocal,确保只有合法变量可访问,避免类型错误。
    • 封装性ThreadLocalMap是内部实现细节,不对外暴露。固定Key类型防止开发者误用,维护代码清晰性。

能否使用其他类型作为Key?

  • 技术上可行但设计上不可取
    • 若强行修改源码替换Key类型(如改为Object),需重新设计哈希机制、内存管理和类型检查,可能破坏现有功能。
    • ThreadLocal类型无法利用弱引用自动清理机制,易导致内存泄漏。
    • 失去唯一性保障,需额外逻辑避免冲突,增加复杂度。

示例说明

假设用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对象了,所以它可以存储任意类型的值。

ThreadExecutor详解

并发容器的使用

多线程结果处理——CompletableFuture

本文由作者按照 CC BY 4.0 进行授权