ThreadLocal概念
ThreadLocal 是 JDK 包提供的,它提供线程本地变量,如果创建 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,实现了线程的数据隔离,从而规避了线程安全问题。
ThreadLocal使用
基本使用
ThreadLocal 基本API包括:构造函数 ThreadLocal()、初始化 initialValue()、设置 set()、访问 get()、删除 remove()。如果 ThreadLocal 中的内容为空时,直接调用 get() 方法,会触发一次 initialValue函数执行。此外当前线执行完之后一般要求调用 remove 释放资源,否则可能造成资源泄漏。
public class Test {
public static ThreadLocal<String> shareVal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
System.out.println("initial run .........");
return Thread.currentThread().getName();
}
};
public static void main(String[] argv) {
System.out.println(shareVal.get());
shareVal.set("hello world");
System.out.println(shareVal.get());
shareVal.remove();
}
}
执行结果:
initial run .........
main
hello world
多线程案例
public class Test {
private static ThreadLocal<String> shareVal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
System.out.println("initial run .........");
return Thread.currentThread().getName();
}
};
private static void print(String str) {
System.out.println(str + " :" + shareVal.get());
shareVal.remove();
}
public static void main(String[] argv) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
shareVal.set("localVar1");
print("thread1");
System.out.println("after remove : " + shareVal.get());
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
shareVal.set("localVar2");
print("thread2");
System.out.println("after remove : " + shareVal.get());
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
执行结果:
thread1 :localVar1
initial run .........
after remove : Thread-0
thread1 :localVar1
initial run .........
after remove : Thread-0
thread1 :localVar1
initial run .........
after remove : Thread-0
thread1 :localVar1
initial run .........
after remove : Thread-0
thread1 :localVar1
initial run .........
after remove : Thread-0
ThreadLocal使用场景
ThreadLocal 最常用的地方就是为每个线程绑定一个数据库连接池中的某个连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
场景1
ThreadLocal 常常用于保存线程不安全的工具类。在这种情况下,每个Thread内都有自己的实例副本,且该副本只能由当前Thread访问到并使用。相比每个线程实例化一个工具类来讲,可以降低资源浪费。因为工具类的创建和销毁是有开销的。
public class Test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int temp = i;
new Thread(new Runnable() {
@Override
public void run() {
// 每个线程都调用 date 函数,date 每次都会 实例化一个 SimpleDateFormat
String data = date(temp);
System.out.println(data);
}
}).start();
Thread.sleep(100);
}
}
private static String date(int seconds){
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
以上是日常工作中最为常见的代码,每个线程需要做时间格式化展示,每个线程都调用 date 函数,date 每次都会 实例化一个 SimpleDateFormat。如果是线程较多,那么 SimpleDateFormat 的创建和销毁多多少少造成资源浪费。ThreadLocal 恰好解决这个问题。
class ThreadSafeFormater {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}
public class Test {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int temp = i;
threadPool.submit(() -> {
String data = date(temp);
System.out.println(data);
});
}
threadPool.shutdown();
}
private static String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormater.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
一般采用线程池来完成较多的任务。以上代码将每个 SimpleDateFormat 实例和线程绑定,线程池中有16个线程,有16个 SimpleDateFormat 对应绑定,大大减少了 SimpleDateFormat 的创建和销毁时造成的资源浪费。
场景2
每个线程内需要保存类似于全局变量的信息(例如数据库线程池中的连接),通常会在 Dao 层获取数据库线程池中的连接来实现资源的增删改查。在 service 层实现对应的业务操作。但是在涉及事务操作时,又必须在 service 获取数据库线程池中的连接来开启事务。且在一个线程内,要求开启事务的连接必须和 Dao 层的连接必须是同一个连接,否则无法达到事务的效果。
public class Service{
// 模拟转账
public void transferAccounts() {
Connection connect = Pool.getConnection();
connect.setAutoCommit(false);
Dao dao = new Dao();
dao.del("小明", 100, connect);
dao.add("小红", 100, connect);
connect.commit();
}
}
public class Dao{
public void del(Sttring person, int money, Connection connect) {
connect.execute("扣除账号余额的sql");
}
public void add(Sttring person, int money, Connection connect) {
connect.execute("增加账号余额的sql");
}
}
从以上伪代码出发,为了保证开启事务的连接必须和 Dao 层的连接必须是同一个连接,将 Service 层中的 connect 直接传递到 Dao,业务繁杂的系统,显然会代码的耦合度急剧升高。恰好 ThreadLocal 可以解决这个问题。
public class JdbcUtils{
private static ThreadLocal<Connection> threadLocalConnect = new ThreadLocal<Connection>()
// 模拟转账
public void getConnection() {
Connection connection = threadLocalConnect.get()
if(connection.get() == null) {
connection = Pool.getConnection();
threadLocalConnect.set(connection);
}
return threadLocalConnect.get();
}
}
public class Service{
// 模拟转账
public void transferAccounts() {
Connection connect = JdbcUtils.getConnection();
connect.setAutoCommit(false);
Dao dao = new Dao();
dao.del("小明", 100);
dao.add("小红", 100);
connect.commit();
}
}
public class Dao{
public void del(Sttring person, int money) {
Connection connect = JdbcUtils.getConnection();
connect.execute("扣除账号余额的sql");
}
public void add(Sttring person, int money) {
Connection connect = JdbcUtils.getConnection();
connect.execute("增加账号余额的sql");
}
}
从以上伪代码出发,将 connect 放置到 ThreadLocal,并且和当前线程绑定,在同一个线程中后续需要用到 connect 的地方直接从 ThreadLocal 实例中获取,大大降低了代码的耦合度。
ThreadLocal总结
理解和正常使用 ThreadLocal 非常重要。一个ThreadLocal 变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰,规避线程安全问题。同时 ThreadLocal 解决了参数在一个线程中各个函数之间互相传递的问题,大大降低了代码的耦合度。