ThreadLocal学习
我们知道,多线程共享数据时可能会存在并发错误,之前一直用synchronized这类线程同步的机制来解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容。在线程同步的机制中,多个线程不能同时访问共享数据,必须先后对变量的值进行访问或者修改,这是一种以延长访问时间来换取线程安全性的策略,必然降低了程序的运行效率。
而ThreadLocal的出现为解决多线程并发问题提供了一种新思路。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。这就意味着竞争条件被彻底消除了,没有必要对这些线程进行同步,它们也就能最大限度的由CPU调度,并发执行。ThreadLocal访问的变量,都是自己独有的变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。与线程同步的机制对比,ThreadLocal是一种以空间来换取线程安全性的策略,运行效率较前者高。
ThreadLocal的接口方法
·void set(Object value)设置当前线程的线程局部变量的值;
·public Object get()该方法返回当前线程所对应的线程局部变量;
·protected Object initialValue()返回该线程局部变量的初始值,是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,缺省实现直接返回一个null。
·JDK 5.0新增了public void remove()方法删除当前线程局部变量的值,目的是为了减少内存的占用,并不是必须的操作,因为当线程结束后,对应该线程的局部变量将自动被垃圾回收,但显示调用该方法能够加快内存回收的速度。
·JDK 5.0出现泛型以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。方法分别是void set(T value)、T get()以及T initialValue()。
【例1】
import java.util.Random;
public class ThreadLocalTest {
private static ThreadLocal<Integer> i = new ThreadLocal<Integer>();// 创建ThreadLocal
public static void main(String[] args) {
for (int x = 0; x < 2; x++) {// 创建2个线程
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();// 获取随机数
System.out.println(Thread.currentThread().getName()
+ "has put a data" + data);
i.set(data);// 将随机数放入当前线程的变量拷贝中
new A().get();// 通过对象A获取当前线程的变量
new B().get();// 通过对象B获取当前线程的变量
}
}).start();
}
}
static class A {
public void get() {
int data = i.get();
System.out.println("A from " + Thread.currentThread().getName()
+ " get data :" + data);
}
}
static class B {
public void get() {
int data = i.get();
System.out.println("B from " + Thread.currentThread().getName()
+ " get data :" + data);
}
}
}
运行结果:
Thread-1 has put a data1646705666
Thread-0 has put a data1387369058
A from Thread-1 get data :1646705666
A from Thread-0 get data :1387369058
B from Thread-0 get data :1387369058
B from Thread-1 get data :1646705666
从运行结果可以看出:通过ThreadLocal创建的变量在多线程中是独立的,不管是通过模块A取出还是通过模式B取出的线程变量,只要线程相同,取出的变量就相同,线程不同,取出的变量也不相同,这正说明了每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
上面的程序中,每个线程的独立副本都是单个变量,如果我要给每个线程设置多个变量副本那怎么做?比如有2个变量副本,分别为整型和字符串类型。
分析:由于ThreadLocal支持泛型(泛型是引用类型),我们可以将这2个变量副本集中到集合或者类中,再设置或获取线程相关的类或集合,进而完成给每个线程存取多个独立变量的任务。
【每个线程独立操作多个变量】
import java.util.Random;
public class ThreadLocalTest {
private static ThreadLocal<Integer> i = new ThreadLocal<Integer>();// 创建ThreadLocal
private static ThreadLocal<ScopeData> scopeData = new ThreadLocal<ScopeData>();
public static void main(String[] args) {
for (int x = 0; x < 2; x++) {// 创建2个线程
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();// 获取随机数
System.out.println(Thread.currentThread().getName()
+ " has put a data " + data);
i.set(data);// 将随机数放入当前线程的变量拷贝中
scopeData.set(new ScopeData());// 往当前线程放入ScopeData对象副本
new A().get();// 通过对象A获取当前线程的变量
new B().get();// 通过对象B获取当前线程的变量
}
}).start();
}
}
static class A {
public void get() {
int data = i.get();// 取出当前线程的随机数
ScopeData instance = scopeData.get();// 取出本线程的ScopeData对象
instance.setAge(data);
instance.setName("name" + data);
System.out.println("A from " + Thread.currentThread().getName()
+ " get age :" + instance.getAge() + " get name :"
+ instance.getName());
}
}
static class B {
public void get() {
int data = i.get();// 取出当前线程的随机数
ScopeData instance = scopeData.get();// 取出本线程的ScopeData对象
instance.setAge(data);
instance.setName("name" + data);
System.out.println("B from " + Thread.currentThread().getName()
+ " get age :" + instance.getAge() + " get name :"
+ instance.getName());
}
}
}
class ScopeData {
private String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
运行结果:
Thread-1 has put a data 1095498667
Thread-0 has put a data -1482596895
A from Thread-0 get age :-1482596895 get name :name-1482596895
A from Thread-1 get age :1095498667 get name :name1095498667
B from Thread-0 get age :-1482596895 get name :name-1482596895
B from Thread-1 get age :1095498667 get name :name1095498667
上面的运行结果证明,ThreadLocal存取的对象是独立的,通过多个模块(A和B)获取的数据都是各自线程的数据,线程之间数据操作不会相互影响。
通过上面2个例子可以看出,ThreadLocal实现的多进程并发不需要同步机制也能消除并发错误,因为它们操作的都是各自的副本,就像线程是隔离的,那它是如何做到的呢?
查看存取数据的set()和get()方法源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从上面源码可以知道,ThreadLocal线程隔离的关键在于ThreadLocal类的一个静态内部类ThreadLocalMap,它实现了键值对的设置和获取(ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。
set()和get()这两个方法的代码可知,在get()当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这和set()方法的代码对应。
至此我们知道了ThreadLocal在多线程环境中不需要同步机制也不会出现并发问题,因为它使得每个线程操作的变量副本都是独立的。回顾之前的单例设计,我们知道懒汉式是存在多线程并发隐患的,需要同步机制解决,现在我们有了ThreadLocal就可以优雅的实现懒汉式单例设计。
【懒汉式单例设计】
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class ThreadLocalTest {
private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
private static ThreadLocal<MyThreadScopeData> myThreadScopeData = new ThreadLocal<MyThreadScopeData>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {// 创建2个线程
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();// 获取一个随机数
System.out.println(Thread.currentThread().getName()
+ " has put data :" + data);
x.set(data);// 将一个随机数放进ThreadLocal<Integer>对象中
MyThreadScopeData.getThreadInstance()// 获取MyThreadScopeData对象
.setName("name" + data);// 设置该对象的姓名为"name" + data
// 字符串
MyThreadScopeData.getThreadInstance().setAge(data);// 获取实例并设置年龄为data
new A().get();// 通过模块A获取x和myThreadScopeData的数据
new B().get();// 通过模块B获取x和myThreadScopeData的数据
}
}).start();
}
}
static class A {
public void get() {
int data = x.get();// 获取当前线程存储的随机数
System.out.println("A from " + Thread.currentThread().getName()
+ " get data :" + data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out
.println("A from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + ","
+ myData.getAge());// 输出当前线程MyThreadScopeData实例的姓名年龄
}
}
static class B {
public void get() {
int data = x.get();// 获取当前线程存储的随机数
System.out.println("B from " + Thread.currentThread().getName()
+ " get data :" + data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out
.println("B from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + ","
+ myData.getAge());// 输出当前线程MyThreadScopeData实例的姓名年龄
}
}
}
// 单例设计
class MyThreadScopeData {
private MyThreadScopeData() {
}
public static MyThreadScopeData getThreadInstance() {
MyThreadScopeData instance = map.get();
if (instance == null) {
instance = new MyThreadScopeData();
map.set(instance);// 如果map没有实例,就创建一个实例放进map中
}
return instance;// 如果map.get()有实例就返回该实例
}
private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
运行结果:
Thread-0 has put data :-477718897
Thread-1 has put data :972697909
A from Thread-1 get data :972697909
A from Thread-1 getMyData: name972697909,972697909
A from Thread-0 get data :-477718897
A from Thread-0 getMyData: name-477718897,-477718897
B from Thread-1 get data :972697909
B from Thread-0 get data :-477718897
B from Thread-1 getMyData: name972697909,972697909
B from Thread-0 getMyData: name-477718897,-477718897
从运行结果可见,不需要同步也能解决并发隐患,所以比同步机制效率高,但占用内存会比同步机制大,是一种空间换时间的方案。