Java基础——ThreadLocal

关于ThreadLocal的文章以及非常多了,有的写的很好,讲的很透彻,这里我再造造轮子,写写自己对ThreadLocal的理解。

1、什么是ThreadLocal?

ThreadLocal从名字上可以简单理解为线程本地变量,也就是说一个变量在每一个使用它的线程中都有一个副本,线程对自己持有的变量副本的访问和修改不会影响其他线程中的副本。

2、ThreadLocal有什么用?

假如有多个任务,每个任务的处理方式和流程都相同,但是每个任务都具有不同的内部状态。为了加快处理速度,现使用多个线程同时处理多个任务,每个线程处理一个任务(每个线程执行相同的代码),那么这种场景就非常适合ThreadLocal。

一个典型例子

在Web项目中,多个用户同时访问后台,每个用户的信息和行为不尽相同。在后台,系统会给每个用户创建一个Session,用于保存用户的信息及行为,以便后台给用户提供个性化服务,这里的Session就非常适合使用ThreadLocal。

再如在Spring中,ThreadLocal被用于作用域为Request的Bean,使得Request(Thread)之间状态互相隔离。

一个简单的例子
package com.tcy.learning.javaee_spring_boot_in_action.capter1;

class Points{
    private int points = 0;

    public Points(int initPoints){
        this.points = initPoints;
    }
    public int getPoints(){
        return points;
    }
    public void addPoints(int newPoints){
        this.points += newPoints;
    }
}

public class Test{
//这里假设points为玩家当前得分
    private ThreadLocal<Points> points = new ThreadLocal<Points>(){
        public Points initialValue() {
            return new Points(0);
        }
    };

    public void startTest(){
        Thread thread1 = new Thread(new PlayThread(10), "thread1");
        Thread thread2 = new Thread(new PlayThread(15), "thread2");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Points等于:"+points.get().getPoints());
    }
    //这里模拟玩家2次得分情况
    class PlayThread implements Runnable{
        private int level;

        public PlayThread(int level){
            this.level = level;
        }

        public void run(){
            int p1 = level;//这里可以改为随机数
            points.get().addPoints(p1);
            int p2 = level + 1; //这里可以改为随机数
            points.get().addPoints(p2);

            int currentPoints = points.get().getPoints();
            String threadName = Thread.currentThread().getName();
            String fmt = "玩家当前分数:%d, 线程:%s";
            String msg = String.format(fmt, currentPoints, threadName);
            System.out.println(msg);
        }
    }

    public static void main(String[] args){
        Test test = new Test();
        test.startTest();
    }
}

输出结果如下(thread1和thread2的顺序有可能不一样):

当前玩家分数:21,线程:thread1
当前玩家分数:31,线程:thread2
Points等于:0

thread1和thread2均对同一个变量(实际上不是同一个变量)points进行了两次add操作,结果points在thread1中是21,在thread2中是31,在main线程中却为0,说明points在三个线程中均有一个副本,副本之间互不干扰。

3、ThreadLocal内部实现原理

我们先从上面的简单例子开始讲起,在thread1和thread2中,points还是同一个对象,被两个线程共享,那为什么两个线程对分数的add互不干扰呢?其实关键就这points.get()函数,让我们看看get方法源码:

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();
}

我们看到get方法首先获取了当前线程,然后使用this(自身)作为Map的key,在ThreadLocalMap中取得一个Entity(Entity继承了WeakReference

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

非常简单,ThreadLocalMap就是当前线程的成员变量threadLocals,那threadLocals又是怎么来的呢?我们看到get方法中,当ThreadLocalMap为null的时候执行l setInitialValue

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

我们先跳过setInitialValue方法中的initialValue,可以看到当map为null的时候调用了createMap

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

createMap方法创建了一个ThreadLocalMap,并将ThreadLocalMap赋给了线程的threadLocals。ThreadLocalMap是ThreadLocal中专门实现的内部类,我们可以简单理解成一个与HashMap类似的Map(实际ThreadLocalMap并没有实现Map接口,与HashMap等Map也没有任何关系),具体实现这里就不多介绍了,有兴趣可以查看ThreadLocal源码。

我们回过头来看看initialValue方法

protected T initialValue() {
    return null;
}

这个方法的源码是不是让人很失望,直接返回了null,其实这个方法需要使用者Override,如果不重写该方法使用会报NullPointException。

ThreadLocal原理总结
  1. ThreadLocal不持有任何值
  2. 真正持有值的是ThreadLocalMap
  3. ThreadLocalMap被Thread持有
  4. ThreadLocal仅作为ThreadLocalMap的key,所以可以通过ThreadLocal从ThreadLocalMap中取出值
为什么在不同的线程中ThreadLocal返回不同的副本

每个线程都有一个ThreadLocalMap,所以每个线程都可以拥有自己的副本,ThreadLocal只从当前线程的ThreadLocalMap存取值(不存在就新创建),所以在不同线程中取出的值是不同的副本。

4、ThreadLocal另类实现
public class AnotherThreadLocal<T> {
    private Map<Thread, T> threadLocalMap = new ConcurrentHashMap<Thread, T>();

    public T get(){
        Thread thread = Thread.currentThread();
        T v = threadLocalMap.get(thread);
        if( v != null){
            return v;
        }
        v = initialValue();
        threadLocalMap.put(thread, v);
        return v;
    }

    public void set(T value){
        Thread thread = Thread.currentThread();
        threadLocalMap.put(thread, value);
    }

    public T initialValue(){
        return null;
    }
}

上述代码能实现ThreadLocal的效果,但是存在两个问题:
- 存在多线程竞争问题,threadLocalMap被线程共享,所以使用线程安全的ConcurrentHashMap,但是会影响性能。ThreadLocal使用的是当前线程自己的ThreadLocalMap,所以不存在竞争,不需要同步。
- 当一些线程结束后(AnotherThreadLocal还被其他线程持有),JVM无法回收已结束线程的对象副本,因为对象被AnotherThreadLocal持有,而不是被线程持有

第一个问题无法避免线程同步,这也体现了ThreadLocal的巧妙之处。第二个问题通过WeakReference的方式解决。

以上是我对ThreadLocal的一些理解,如有问题欢迎提出交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值