多线程ThreadLocal

1.什么ThreadLocal关键字

ThreadLocal类是用来提供线程内部的局部变量.这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量能相对于独立其他线程内的变量.ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文.

总结:

  1. 线程并发:在多线程并发的场景下
  2. 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  3. 线程隔离:每个线程的变量都是独立的,不会互相影响

2.基本使用

简单看一个案例,在这段代码中我们可以发现线程是不隔离的,所有线程都获得了线程4的数据.

public class MyDemo01 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo01 myDemo01 = new MyDemo01();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                myDemo01.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println("------------------------------");
                System.out.println(Thread.currentThread().getName() + "----->" + myDemo01.getContent());
            }).start();
        }
    }
}

我们可以用ThreadLocal解决这个问题

public class MyDemo {
    ThreadLocal<String> tl = new ThreadLocal<>();
    private String content;

    public String getContent() {
        return tl.get();
    }

    public void setContent(String content) {
        tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo myDemo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                myDemo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println("------------------------------");
                System.out.println(Thread.currentThread().getName() + "----->" + myDemo.getContent());
            }).start();
        }
    }
}

当然我们熟悉的synchnorized关键字也是可以解决的

public class MyDemo02 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo02 myDemo01 = new MyDemo02();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (MyDemo02.class) {
                    myDemo01.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("------------------------------");
                    System.out.println(Thread.currentThread().getName() + "----->" + myDemo01.getContent());
                }
            }).start();
        }
    }
}

 synchnorizedThreadLocal
原理同步机制采用以时间换空间的方式,只提供了一份变量,让不同线程排队访问ThreadLocal采用"以空间换时间"的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而不相干扰
侧重点多个线程之间访问资源的同步多个线程之间让每个线程之间的数据相互隔离

 

虽然我们可以采用synchronized和ThreadLocal都可以解决这个问题,但是ThreadLocal无疑拥有更好的性能,synchronized毕竟加了锁,性能会差一些,并发性不如ThreadLocal!

3.ThreadLocal的使用场景

参考https://blog.csdn.net/yizhenn/article/details/52384520

https://blog.csdn.net/weixin_30871293/article/details/97703920

3.1数据库连接池

比如一次请求线程进来,业务 Dao 需要更新 user 表和 user-detail 表。如果是 new 出两个数据库 Connection ,分别不同的 Connection 操作 user 表和 user-detail 表,就无法保证事务。那么数据库连接池是如何保证的?

答案是:利用 ThreadLocal 存储唯一 Connection 对象。每次请求线程,pool.getConnection 获取连接的时候都会这样操作:

  • 会从 ThreadLocal 获取 Connection 对象。如果有,则保证了后面多个数据库操作共用同一个 Connection ,从而保证了事务。
  • 如果没有,往 ThreadLocal 新增Connection 对象,并返回到线程

使用 ThreadLocal 保证每个请求线程的 Connection 是唯一的。即每个线程有自己的连接。

Spring 框架,在事务开始时,会给当前线程一个Jdbc Connection,在整个事务过程,都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

3.2Web系统Session的存储

Web容器采用线程隔离的多线程模型,也就是每一个请求都会对应一条线程,线程之间相互隔离,没有共享数据。这样能够简化编程模型,程序员可以用单线程的思维开发这种多线程应用。

当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可.

3.3打印日志

日志的打印(同一线程的日志一起打印,或者说一次事务的日志一起打印,因为一般默认一次事务都是由同一个线程执行的,将事务的日志保存在线程局部变量当中,当事务执行完成的时候统一打印)

4.ThreadLocal的结构

要分析ThreadLocal的结构,我们先简单看一下它的源码

public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

public class Thread implements Runnable {

    //与此线程有关的ThreadLocal值。 该map的维护
    //由ThreadLocal类提供
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //这部分是做线程传递用的
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

}

我们看一下ThreadLocalMap 是个什么东西

 //ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储
static class ThreadLocalMap {

        //Entry是继承WeakReference(弱引用)
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

再看一下get方法

public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取它对应的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
             //获取key(当前的ThreadLocal)对应的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //threadLocals为null,调用该方法更改初始化当前线程的threadLocals变量
        return setInitialValue();
    }

简单做个总结:

  1. 每个Thread维护着一个ThreadLocalMap的引用
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

5.ThreadLocal的内存泄露

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了(垃圾回收线程一旦发现弱引用就会立即回收该对象)。ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。我们可以通过在代码的最后使用remove就可以解决这个问题.

如果 key 是强引用,那么发生 GC 时 ThreadLocalMap 还持有 ThreadLocal 的强引用,会导致 ThreadLocal 不会被回收,从而导致内存泄漏。弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 方法时被清除,这算是最优的解决方案.

参考

https://segmentfault.com/a/1190000019952281

https://blog.csdn.net/qq_42405666/article/details/108258820

https://blog.csdn.net/iteye_10289/article/details/82404562

https://zhuanlan.zhihu.com/p/34494674

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值