一、概述
本文首先介绍一个多线程编程的例子,以此展现不用ThreadLocal会出现的问题,然后不断的重构,最终自己实现一个ThreadLocal。
因为跟多线程编程有关,所以很多初学者把ThreadLocal当成很神秘的东西。其实ThreadLocal就是一个普普通通的类,该类的作用仅仅是为了方便我们操作Thread类中的属性。
二、手写ThreadLocal
1、基础代码
我们要实现这样的功能,在每个线程的开头保存username,然后线程执行过程中将把这个username打印出来。我们定义一个username的全局变量模拟业务中将会并发访问的通用数据结构,sleep方法模拟业务逻辑执行时长,printUsername模拟多线程中用到username这个全局变量,以下是基础代码:
public class Test {
/** 用户名,定义成全局变量存在并发问题,本文将就此问题不断的进行重构 */
public static String username;
/**
* 休眠多少毫秒,我们借此模拟程序执行业务逻辑花费了一定时长
* @param millis
*/
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 打印username
* 在这个简单的例子中,我们当然可以直接将username作为参数传递进来,真实的业务场景,
* 调用链条往往比较长,每个方法都传递一个像username这样的通用参数不是一个好的做法,
* 而应该将username作为一个全局变量
*/
public static void printUsername() {
System.out.println("thread: " + Thread.currentThread().getName()
+ ", username: " + username);
}
}
2、单个线程,一切正常
创建一个线程,线程开头设置了username的值,中间处理其他业务逻辑花了1秒,然后打印username,一切正常
public static void main(String[] args) {
new Thread(() -> {
username = "pete1";
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}).start();
}
3、两个线程,问题来了
我们再创建一个线程2,这个线程执行业务逻辑只花了1毫秒
public static void main(String[] args) {
new Thread(() -> {
username = "pete1";
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程1").start();
new Thread(() -> {
username = "pete2";
sleep(1); // 中间执行其他逻辑花了1毫秒
printUsername();
}, "线程2").start();
}
我们看下打印的内容:
thread: 线程2, username: pete1
thread: 线程1, username: pete1
或者
thread: 线程2, username: pete2
thread: 线程1, username: pete2
打印的username可能都是pete1,也可能都是pete2,这很明显不是我们想要的结果,我们希望线程1打印pete1,线程2打印pete2。这是因为username是全局变量,两个线程的开头都设置了username的值,后设置的值将会覆盖前面设置的值,至于pete1、pete2哪个先设置跟系统调度有关,我们不去讨论如何控制他们的先后关系,此问题与本文讨论的主题无关。
4、用Map封装,Key为当前线程,Value为username
我们要实现每个线程的username是独立的,不能修改、访问别的线程的username,我们可以用Map进行封装,Key为当前线程,Value为username,代码如下
// String -> Map<Thread, String>
public static Map<Thread /*当前线程*/, String /*用户名*/> usernames = new HashMap<>();
// username -> usernames.get(Thread.currentThread())
public static void printUsername() {
System.out.println("thread: " + Thread.currentThread().getName()
+ ", username: " + usernames.get(Thread.currentThread()));
}
// username = ... -> usernames.put(...)
public static void main(String[] args) {
new Thread(() -> {
usernames.put(Thread.currentThread(), "pete1");
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程1").start();
new Thread(() -> {
usernames.put(Thread.currentThread(), "pete2");
sleep(1); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程2").start();
}
运行以上,线程1对应的username是pete1,线程2对应的username是pete2,完美,大功告成!
不过认真想下,这样的代码是非常不安全的,线程1可以很容易把其他线程的值改掉,甚至usernames.clear()一切归零,这显然不是我们想要的,我们希望每个线程只能修改自身的值而不要去影响其他线程。
5、将username封装到线程类中
上面说到不想让某个线程的操作影响到其他线程,可以考虑将变量封装到线程类中,由于我们无法修改Thread的代码,因此我们定义一个MyThread类继承Thread,后续创建线程的操作都是基于MyThread类,代码如下:
/**
* 定义一个静态内部类继承Thread,封装了username
*/
public static class MyThread extends Thread {
private String username;
public MyThread(Runnable target, String name) {
super(target, name);
}
}
创建线程用MyThread,设置与获取username都用MyThread中的username
public static void printUsername() {
System.out.println("thread: " + Thread.currentThread().getName()
+ ", username: " + ((MyThread) Thread.currentThread()).username);
}
public static void main(String[] args) {
new MyThread(() -> {
((MyThread) Thread.currentThread()).username = "pete1";
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程1").start();
new MyThread(() -> {
((MyThread) Thread.currentThread()).username = "pete2";
sleep(1); // 中间执行其他逻辑花了1毫秒
printUsername();
}, "线程2").start();
}
运行程序,线程1的username为pete1,线程2的username为pete2,完美,大功告成。
除了username,如果我们还要再加一个变量age呢,而且要加多少变量是不确定的,所以我们应该设计的更通用一些。
6、将线程内的变量定义成Map<String, Object>
上面我们说到直接在MyThread中定义username不够通用,如果以后要加age变量或者其他的就比较麻烦,我们应该将此变量定义的足够通用,因此改成Map<String, Object>,key为变量名,value为对应的值,例如有username与age两个变量,Map中的值就是:“username”:“pete1”, “age”: 29,代码如下:
public static class MyThread extends Thread {
/** 用于保存各个线程独立的变量 */
private Map<String, Object> threadLocals = new HashMap<>();
public MyThread(Runnable target, String name) {
super(target, name);
}
}
存取username的代码全部改成通过MyThread的threadLocals,代码如下:
public static void printUsername() {
System.out.println("thread: " + Thread.currentThread().getName()
+ ", username: "
+ ((MyThread) Thread.currentThread()).threadLocals.get("username"));
}
public static void main(String[] args) {
new MyThread(() -> {
((MyThread) Thread.currentThread()).threadLocals.put("username", "pete1");
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程1").start();
new MyThread(() -> {
((MyThread) Thread.currentThread()).threadLocals.put("username", "pete2");
sleep(1); // 中间执行其他逻辑花了1毫秒
printUsername();
}, "线程2").start();
}
以上代码可以很好的实现我们想要的功能,但是这样的代码太冗长了,可读性不好:((MyThread)Thread.currentThread()).threadLocals.put(“username”,“pete1”)。
7、定义MyThreadLocal简化线程局部变量的操作
上面说到,读写线程局部变量的代码可读性不好,我们需要简化,我们封装一个MyThreadLocal类简化这些操作,代码如下:
public static class MyThreadLocal<T> {
private String varName;
public MyThreadLocal(String varName) {
this.varName = varName;
}
public void set(T value) {
((MyThread) Thread.currentThread()).threadLocals.put(varName, value);
}
public T get() {
return (T) ((MyThread) Thread.currentThread()).threadLocals.get(varName);
}
}
简化后的局部变量读写代码如下:
// 定义一个usernameHolder的全局变量,用于简化操作线程中的threadLocals
public static MyThreadLocal<String> usernameHolder = new MyThreadLocal<>("username");
// 通过usernameHolder.get()获取MyThread中的threadLocals的值
public static void printUsername() {
System.out.println("thread: " + Thread.currentThread().getName()
+ ", username: " + usernameHolder.get());
}
// 通过usernameHolder.set()设置MyThread中的threadLocals的值
public static void main(String[] args) {
new MyThread(() -> {
usernameHolder.set("pete1");
sleep(1000); // 中间执行其他逻辑花了1秒
printUsername();
}, "线程1").start();
new MyThread(() -> {
usernameHolder.set("pete2");
sleep(1); // 中间执行其他逻辑花了1毫秒
printUsername();
}, "线程2").start();
}
8、threadLocals的key改用MyThreadLocal实例
我们之前用字符串来标识一个变量,因此threadLocals定义成Map<String, Object>,也就是key是一个String类型,现在我们有了MyThreadLocal类,我们可以直接使用MyThreadLocal实例来标识变量,代码如下:
// Map<String, Object> -> Map<MyThreadLocal<?>, Object>
public static class MyThread extends Thread {
private Map<MyThreadLocal<?>, Object> threadLocals = new HashMap<>();
public MyThread(Runnable target, String name) {
super(target, name);
}
}
// put(varName, value) -> put(this, value)
// get(varName) -> get(this)
public static class MyThreadLocal<T> {
public void set(T value) {
((MyThread) Thread.currentThread()).threadLocals.put(this, value);
}
public T get() {
return (T) ((MyThread) Thread.currentThread()).threadLocals.get(this);
}
}
// new MyThreadLocal<>("username") -> new MyThreadLocal<>()
public static MyThreadLocal<String> usernameHolder = new MyThreadLocal<>();
9、总结
至此我们实现了一个简单的ThreadLocal类,JDK自带的ThreadLocal类的原理与本文所述相似。通过以上内容可以看出其实ThreadLocal就是一个普普通通的类,该类的作用仅仅是为了方便我们操作Thread类中的属性,没有ThreadLocal类也一样可以,就是代码比较冗长。关于ThreadLocal还有很多值得说的,我们下篇再见。