一、简介

ThreadLocal类表示线程局部变量,其实例通常是用来存放与线程相关联的状态的私有静态字段,通过把数据放在ThreadLocal中就可以让每条线程创建一个此变量的副本,每个线程都可以独立地改变自己的副本,从而避免并发访问的线程安全问题。

ThreadLocal和其他所有的同步机制都是为了解决多线程中对同一变量的访问冲突。在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问,这种方式中变量是多个线程共享的,系统不会将资源复制多份。而ThreadLocal是将需要并发访问的资源复制多份,每个线程拥有自己的资源副本;使用ThreadLocal时,可以把不安全的整个变量或把共享对象与线程相关的状态保存到ThreadLocal中。

ThreadLocal并不能代替同步机制,它们面向的问题领域不同:同步机制是为了同步多个线程对相同资源的并发访问,是多线程之间进行通信的有效方式;而ThreadLocal是隔离多线程的数据共享,使用副本的方式避免了资源的共享。如果需要在多个线程之间共享资源来实现线程通信,则使用同步机制;如果仅仅是需要隔离多个线程之间的共享冲突,则使用ThreadLocal。

二、常用方法

1、set

public void set(T value)

设置线程局部变量(thread-local)中当前线程副本的值。大多数子类不需要重写此方法,使用initialValue()方法来设置线程局部变量的值。

2、get

public T get()

返回当前线程在线程局部变量(thread-local)副本中的值。如果在线程局部变量中没有当前线程的值,则首先将其初始化为调用initialValue方法返回的值。

3、remove

public void remove()

删除线程局部变量(thread-local)中当前线程的值。之后如果当前线程再次读取此线程局部变量,则将调用initialValue方法重新初始化值,除非它的值临时被当前线程设置。这可能会导致在当前线程中多次调用initialValue方法。

4、initialValue

protected T initialValue()

返回当前线程在线程局部变量(thread-local)中的初始值。如果在第一次调用get()方法访问变量前没有调用set()方法,则会调用此方法初始化值。通常情况下,每个线程最多调用一次此方法,但如果后来调用了remove()方法后再次调用get()方法也可以再次执行此方法。

此方法默认返回null,如果希望线程局部变量具有非null的初始值,则必须对ThreadLocal类进行子类化(继承ThreadLocal),并且重写此方法,通常使用匿名内部类。

三、样例

1、样例一

不使用ThreadLocal的情况:例子中每个线程运行时都会修改code的值为线程名。

  • MyRunnable
public class MyRunnable implements Runnable{

	private String code;
	
	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		setCode(name);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(String.format("Thread: %s, Code: %s", name, getCode()));
	}

	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}
}
  • ThreadTest
public class ThreadTest {

	public static void main(String[] args) {
		Runnable runnable = new MyRunnable();
		Thread thread1 = new Thread(runnable, "A");
		Thread thread2 = new Thread(runnable, "B");
		Thread thread3 = new Thread(runnable, "C");
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

运行输出:

Thread: C, Code: C
Thread: B, Code: C
Thread: A, Code: C

输出顺序不定,但每个线程的code值都为C。

2、样例二

使用ThreadLocal:

  • RunnableImpl
public class RunnableImpl implements Runnable{

	private final ThreadLocal<String> code = new ThreadLocal<>();
	
	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		code.set(name);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(String.format("Thread: %s, Code: %s", name, code.get()));
	}

}
  • ThreadTest
public class ThreadTest {

	public static void main(String[] args) {
		Runnable runnable = new RunnableImpl();
		Thread thread1 = new Thread(runnable, "A");
		Thread thread2 = new Thread(runnable, "B");
		Thread thread3 = new Thread(runnable, "C");
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

运行输出:

Thread: C, Code: C
Thread: A, Code: A
Thread: B, Code: B

输出顺序不定,但每个线程取到的code值都是不同的。

3、样例三

为每个线程生成唯一的标识符:每个线程的标识符ID在第一次调用get()方法时分配,并在后续调用中保持不变。此样例中使用initialValue()方法设置线程局部变量的初始值。

  • ThreadId
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {

	private static final AtomicInteger nextId = new AtomicInteger(0);
	
	private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>(){
		@Override
		protected Integer initialValue() {
			return nextId.getAndIncrement();
		}
	};
	
	public static int get(){
		return threadId.get();
	}
	
}
  • MyRunnable
public class MyRunnable implements Runnable{

	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		System.out.println(String.format("Thread %s: %s", name, ThreadId.get()));
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(String.format("Thread %s: %s", name, ThreadId.get()));
	}

}
  • ThreadTest
public class ThreadTest {

	public static void main(String[] args) {
		MyRunnable runnable = new MyRunnable();
		Thread t1 = new Thread(runnable, "A");
		Thread t2 = new Thread(runnable, "B");
		
		t1.start();
		t2.start();
	}
}

运行输出:

Thread A: 0
Thread B: 1
Thread B: 1
Thread A: 0

输出顺序不定,但线程A取到的ID值总是0,线程B取到的ID值总是1。

参考资料: