目录
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以包含一个或多个线程,每条线程并行执行不同的任务。
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。

Java提供了三种创建线程的方法
下表列出了Thread类的一些重要方法:
| 序号 | 方法描述 |
| 1 | public void start() |
| 2 | public void run() |
| 3 | public final void setName(String name) |
| 4 | public final void setPriority(int priority) |
| 5 | public final void setDaemon(boolean on) |
| 6 | public final void join(long millisec) |
| 7 | public void interrupt() |
| 8 | public final boolean isAlive() |
上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。
| 序号 | 方法描述 |
| 1 | public static void yield() |
| 2 | public static void sleep(long millisec) |
| 3 | public static boolean holdsLock(Object x) |
| 4 | public static Thread currentThread() |
| 5 | public static void dumpStack() |
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。
为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
- class RunnableDemo implements Runnable {
- private Thread t;
- private String threadName;
-
- RunnableDemo( String name) {
- threadName = name;
- System.out.println("Creating " + threadName );
- }
-
- public void run() {
- System.out.println("Running " + threadName );
- try {
- for(int i = 4; i > 0; i--) {
- System.out.println("Thread: " + threadName + ", " + i);
- // 让线程睡眠一会
- Thread.sleep(50);
- }
- }catch (InterruptedException e) {
- System.out.println("Thread " + threadName + " interrupted.");
- }
- System.out.println("Thread " + threadName + " exiting.");
- }
-
- public void start () {
- System.out.println("Starting " + threadName );
- if (t == null) {
- t = new Thread (this, threadName);
- t.start ();
- }
- }
- }
-
- public class TestThread {
-
- public static void main(String args[]) {
- RunnableDemo R1 = new RunnableDemo( "Thread-1");
- R1.start();
-
- RunnableDemo R2 = new RunnableDemo( "Thread-2");
- R2.start();
- }
- }
程序运行结果:
- Creating Thread-1
- Starting Thread-1
- Creating Thread-2
- Starting Thread-2
- Running Thread-1
- Thread: Thread-1, 4
- Running Thread-2
- Thread: Thread-2, 4
- Thread: Thread-1, 3
- Thread: Thread-2, 3
- Thread: Thread-1, 2
- Thread: Thread-2, 2
- Thread: Thread-1, 1
- Thread: Thread-2, 1
- Thread Thread-1 exiting.
- Thread Thread-2 exiting.
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
- class ThreadDemo extends Thread {
- private Thread t;
- private String threadName;
-
- ThreadDemo( String name) {
- threadName = name;
- System.out.println("Creating " + threadName );
- }
-
- public void run() {
- System.out.println("Running " + threadName );
- try {
- for(int i = 4; i > 0; i--) {
- System.out.println("Thread: " + threadName + ", " + i);
- // 让线程睡眠一会
- Thread.sleep(50);
- }
- }catch (InterruptedException e) {
- System.out.println("Thread " + threadName + " interrupted.");
- }
- System.out.println("Thread " + threadName + " exiting.");
- }
-
- public void start () {
- System.out.println("Starting " + threadName );
- if (t == null) {
- t = new Thread (this, threadName);
- t.start ();
- }
- }
- }
-
- public class TestThread {
-
- public static void main(String args[]) {
- ThreadDemo T1 = new ThreadDemo( "Thread-1");
- T1.start();
-
- ThreadDemo T2 = new ThreadDemo( "Thread-2");
- T2.start();
- }
- }
编译以上程序运行结果如下:
- Creating Thread-1
- Starting Thread-1
- Creating Thread-2
- Starting Thread-2
- Running Thread-1
- Thread: Thread-1, 4
- Running Thread-2
- Thread: Thread-2, 4
- Thread: Thread-1, 3
- Thread: Thread-2, 3
- Thread: Thread-1, 2
- Thread: Thread-2, 2
- Thread: Thread-1, 1
- Thread: Thread-2, 1
- Thread Thread-1 exiting.
- Thread Thread-2 exiting.
- public class CallableThreadTest implements Callable<Integer> {
- public static void main(String[] args)
- {
- CallableThreadTest ctt = new CallableThreadTest();
- FutureTask<Integer> ft = new FutureTask<>(ctt);
- for(int i = 0;i < 100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
- if(i==20)
- {
- new Thread(ft,"有返回值的线程").start();
- }
- }
- try
- {
- System.out.println("子线程的返回值:"+ft.get());
- } catch (InterruptedException e)
- {
- e.printStackTrace();
- } catch (ExecutionException e)
- {
- e.printStackTrace();
- }
-
- }
- @Override
- public Integer call() throws Exception
- {
- int i = 0;
- for(;i<100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- return i;
- }
- }
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
用一个状态转移图表示如下:
- ┌─────────────┐
- │ New │
- └─────────────┘
- │
- ▼
- ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
- ┌─────────────┐ ┌─────────────┐
- ││ Runnable │ │ Blocked ││
- └─────────────┘ └─────────────┘
- │┌─────────────┐ ┌─────────────┐│
- │ Waiting │ │Timed Waiting│
- │└─────────────┘ └─────────────┘│
- ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
- │
- ▼
- ┌─────────────┐
- │ Terminated │
- └─────────────┘
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:
一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:
- public class Main {
- public static void main(String[] args) throws InterruptedException {
- Thread t = new Thread(() -> {
- System.out.println("hello");
- });
- System.out.println("start");
- t.start();
- t.join();
- System.out.println("end");
- }
- }
当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。
如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
如果多个线程同时读写共享变量,会出现数据不一致的问题。
一个例子:
- public class Main {
- public static void main(String[] args) throws Exception {
- var add = new AddThread();
- var dec = new DecThread();
- add.start();
- dec.start();
- add.join();
- dec.join();
- System.out.println(Counter.count);
- }
- }
-
- class Counter {
- public static int count = 0;
- }
-
- class AddThread extends Thread {
- public void run() {
- for (int i=0; i<10000; i++) { Counter.count += 1; }
- }
- }
-
- class DecThread extends Thread {
- public void run() {
- for (int i=0; i<10000; i++) { Counter.count -= 1; }
- }
- }
上面的代码很简单,两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
例如,对于语句:
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
- ILOAD
- IADD
- ISTORE
我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:
- ┌───────┐ ┌───────┐
- │Thread1│ │Thread2│
- └───┬───┘ └───┬───┘
- │ │
- │ILOAD (100) │
- │ │ILOAD (100)
- │ │IADD
- │ │ISTORE (101)
- │IADD │
- │ISTORE (101)│
- ▼ ▼
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
- ┌───────┐ ┌───────┐
- │Thread1│ │Thread2│
- └───┬───┘ └───┬───┘
- │ │
- │-- lock -- │
- │ILOAD (100) │
- │IADD │
- │ISTORE (101) │
- │-- unlock -- │
- │ │-- lock --
- │ │ILOAD (101)
- │ │IADD
- │ │ISTORE (102)
- │ │-- unlock --
- ▼ ▼
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:
- synchronized(lock) {
- n = n + 1;
- }
synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:
- public class Main {
- public static void main(String[] args) throws Exception {
- var add = new AddThread();
- var dec = new DecThread();
- add.start();
- dec.start();
- add.join();
- dec.join();
- System.out.println(Counter.count);
- }
- }
-
- class Counter {
- public static final Object lock = new Object();
- public static int count = 0;
- }
-
- class AddThread extends Thread {
- public void run() {
- for (int i=0; i<10000; i++) {
- synchronized(Counter.lock) {
- Counter.count += 1;
- }
- }
- }
- }
-
- class DecThread extends Thread {
- public void run() {
- for (int i=0; i<10000; i++) {
- synchronized(Counter.lock) {
- Counter.count -= 1;
- }
- }
- }
- }
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
我们来概括一下如何使用synchronized:
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:
- public class Counter {
- private int count = 0;
-
- public void add(int n) {
- synchronized(this) {
- count += n;
- }
- }
-
- public void dec(int n) {
- synchronized(this) {
- count -= n;
- }
- }
-
- public int get() {
- return count;
- }
- }
这样一来,线程调用add()、dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:
- var c1 = Counter();
- var c2 = Counter();
-
- // 对c1进行操作的线程:
- new Thread(() -> {
- c1.add();
- }).start();
- new Thread(() -> {
- c1.dec();
- }).start();
-
- // 对c2进行操作的线程:
- new Thread(() -> {
- c2.add();
- }).start();
- new Thread(() -> {
- c2.dec();
- }).start();
现在,对于Counter类,多线程可以正确调用。
当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:
- public void add(int n) {
- synchronized(this) { // 锁住this
- count += n;
- } // 解锁
- }
- public synchronized void add(int n) { // 锁住this
- count += n;
- } // 解锁
因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:
- class TaskQueue {
- Queue<String> queue = new LinkedList<>();
-
- public synchronized void addTask(String s) {
- this.queue.add(s);
- }
- }
但是synchronized并没有解决多线程协调的问题。
仍然以上面的TaskQueue为例,我们再编写一个getTask()方法取出队列的第一个任务:
- class TaskQueue {
- Queue
queue = new LinkedList<>(); -
- public synchronized void addTask(String s) {
- this.queue.add(s);
- }
-
- public synchronized String getTask() {
- while (queue.isEmpty()) {
- }
- return queue.remove();
- }
- }
上述代码看上去没有问题:getTask()内部先判断队列是否为空,如果为空,就循环等待,直到另一个线程往队列中放入了一个任务,while()循环退出,就可以返回队列的元素了。
但实际上while()循环永远不会退出。因为线程在执行while()循环时,已经在getTask()入口获取了this锁,其他线程根本无法调用addTask(),因为addTask()执行条件也是获取this锁。
因此,执行上述代码,线程会在getTask()中因为死循环而100%占用CPU资源。
如果深入思考一下,我们想要的执行效果是:
因此,多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
对于上述TaskQueue,我们先改造getTask()方法,在条件不满足时,线程进入等待状态:
- public synchronized String getTask() {
- while (queue.isEmpty()) {
- this.wait();
- }
- return queue.remove();
- }
当一个线程执行到getTask()方法内部的while循环时,它必定已经获取到了this锁,此时,线程执行while条件判断,如果条件成立(队列为空),线程将执行this.wait(),进入等待状态。
这里的关键是:wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()。
调用wait()方法后,线程进入等待状态,wait()方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句。
有些仔细的童鞋会指出:即使线程在getTask()内部等待,其他线程如果拿不到this锁,照样无法执行addTask(),肿么办?
这个问题的关键就在于wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
因此,只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:
- public synchronized String getTask() {
- while (queue.isEmpty()) {
- // 释放this锁:
- this.wait();
- // 重新获取this锁
- }
- return queue.remove();
- }
当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在addTask()方法获得this锁。
现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下:
- public synchronized void addTask(String s) {
- this.queue.add(s);
- this.notify(); // 唤醒在this锁等待的线程
- }
注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。
我们来看一个完整的例子:
- public class Main {
- public static void main(String[] args) throws InterruptedException {
- var q = new TaskQueue();
- var ts = new ArrayList<Thread>();
- for (int i=0; i<5; i++) {
- var t = new Thread() {
- public void run() {
- // 执行task:
- while (true) {
- try {
- String s = q.getTask();
- System.out.println("execute task: " + s);
- } catch (InterruptedException e) {
- return;
- }
- }
- }
- };
- t.start();
- ts.add(t);
- }
- var add = new Thread(() -> {
- for (int i=0; i<10; i++) {
- // 放入task:
- String s = "t-" + Math.random();
- System.out.println("add task: " + s);
- q.addTask(s);
- try { Thread.sleep(100); } catch(InterruptedException e) {}
- }
- });
- add.start();
- add.join();
- Thread.sleep(100);
- for (var t : ts) {
- t.interrupt();
- }
- }
- }
-
- class TaskQueue {
- Queue<String> queue = new LinkedList<>();
-
- public synchronized void addTask(String s) {
- this.queue.add(s);
- this.notifyAll();
- }
-
- public synchronized String getTask() throws InterruptedException {
- while (queue.isEmpty()) {
- this.wait();
- }
- return queue.remove();
- }
- }
这个例子中,我们重点关注addTask()方法,内部调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
但是,注意到wait()方法返回时需要重新获得this锁。假设当前有3个线程被唤醒,唤醒后,首先要等待执行addTask()的线程结束此方法后,才能释放this锁,随后,这3个线程中只能有一个获取到this锁,剩下两个将继续等待。
再注意到我们在while()循环中调用wait(),而不是if语句:
- public synchronized String getTask() throws InterruptedException {
- if (queue.isEmpty()) {
- this.wait();
- }
- return queue.remove();
- }
这种写法实际上是错误的,因为线程被唤醒时,需要再次获取this锁。多个线程被唤醒后,只有一个线程能获取this锁,此刻,该线程执行queue.remove()可以获取到队列的元素,然而,剩下的线程如果获取this锁后执行queue.remove(),此刻队列可能已经没有任何元素了,所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:
- while (queue.isEmpty()) {
- this.wait();
- }
所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。
Java的线程锁是可重入的锁。
什么是可重入的锁?我们还是来看例子:
- public class Counter {
- private int count = 0;
-
- public synchronized void add(int n) {
- if (n < 0) {
- dec(-n);
- } else {
- count += n;
- }
- }
-
- public synchronized void dec(int n) {
- count += n;
- }
- }
观察synchronized修饰的add()方法,一旦线程执行到add()方法内部,说明它已经获取了当前实例的this锁。如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁,现在问题来了:
对同一个线程,能否在获取到锁以后继续获取同一个锁?
答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
死锁
一个线程可以获取一个锁后,再继续获取另一个锁。例如:
- public void add(int m) {
- synchronized(lockA) { // 获得lockA的锁
- this.value += m;
- synchronized(lockB) { // 获得lockB的锁
- this.another += m;
- } // 释放lockB的锁
- } // 释放lockA的锁
- }
-
- public void dec(int m) {
- synchronized(lockB) { // 获得lockB的锁
- this.another -= m;
- synchronized(lockA) { // 获得lockA的锁
- this.value -= m;
- } // 释放lockA的锁
- } // 释放lockB的锁
- }
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()和dec()方法时:
随后:
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:
- public void dec(int m) {
- synchronized(lockA) { // 获得lockA的锁
- this.value -= m;
- synchronized(lockB) { // 获得lockB的锁
- this.another -= m;
- } // 释放lockB的锁
- } // 释放lockA的锁
- }
ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
我们可以在代码中调用Thread.currentThread()获取当前线程。例如,打印日志时,可以同时打印出当前线程的名字:
- public class Main {
- public static void main(String[] args) throws Exception {
- log("start main...");
- new Thread(() -> {
- log("run task...");
- }).start();
- new Thread(() -> {
- log("print...");
- }).start();
- log("end main.");
- }
-
- static void log(String s) {
- System.out.println(Thread.currentThread().getName() + ": " + s);
- }
- }
对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:
- public void process(User user) {
- checkPermission();
- doWork();
- saveStatus();
- sendResponse();
- }
然后,通过线程池去执行这些任务。
观察process()方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?
这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。
Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
ThreadLocal实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
它的典型使用方式如下:
- void processUser(user) {
- try {
- threadLocalUser.set(user);
- step1();
- step2();
- } finally {
- threadLocalUser.remove();
- }
- }
通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:
- void step1() {
- User u = threadLocalUser.get();
- log();
- printUser();
- }
-
- void log() {
- User u = threadLocalUser.get();
- println(u.name);
- }
-
- void step2() {
- User u = threadLocalUser.get();
- checkUser(u.id);
- }
注意到普通的方法调用一定是同一个线程执行的,所以,step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。
实际上,可以把ThreadLocal看成一个全局Map
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。
最后,特别注意ThreadLocal一定要在finally中清除:
- try {
- threadLocalUser.set(user);
- ...
- } finally {
- threadLocalUser.remove();
- }
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {...}结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:
- public class UserContext implements AutoCloseable {
-
- static final ThreadLocal<String> ctx = new ThreadLocal<>();
-
- public UserContext(String user) {
- ctx.set(user);
- }
-
- public static String currentUser() {
- return ctx.get();
- }
-
- @Override
- public void close() {
- ctx.remove();
- }
- }
使用的时候,我们借助try (resource) {...}结构,可以这么写:
- try (var ctx = new UserContext("Bob")) {
- // 可任意调用UserContext.currentUser():
- String currentUser = UserContext.currentUser();
- } // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
这样就在UserContext中完全封装了ThreadLocal,外部代码在try (resource) {...}内部可以随时调用UserContext.currentUser()获取当前线程绑定的用户名。