通过手写ThreadLocal深入理解其本质

本文通过逐步重构代码,演示了如何从一个简单的全局变量问题出发,引入ThreadLocal的概念,以解决多线程环境下数据隔离的问题。首先,通过全局变量引发的并发问题引出ThreadLocal的必要性,接着通过Map和自定义线程类实现线程局部变量,最后封装成MyThreadLocal类,简化了线程局部变量的读写操作,展示了ThreadLocal的基本工作原理。
摘要由CSDN通过智能技术生成

一、概述

本文首先介绍一个多线程编程的例子,以此展现不用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还有很多值得说的,我们下篇再见。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值