并发编程笔记(二)----ThreadLocal

ThreadLocal理解

通过和synchonized比较来理解threadlocal

threadlocal和synchonized区别:
我们知道它俩都是为了解决多线程的并发访问,但是可是有本质区别的。synchonized主要是利用锁的机制,让变量或者代码块在某一时刻仅仅能被一个线程访问。threadlocal能够为每个小城提供变量的副本,使得每个线程在某一时间访问到的并非同一个对象。用于解决多线程并发访问

例如:spring中的事务
它就是借助了ThreadLocal类实现spring会从数据库连接池中获得一个connection,然后把它放入到ThreadLocal,也就和线程绑定了,事务需要提交或者回滚,只需要从threadlocal中拿connection进行操作。
为什么要这么做呢?因为我们开发一般采用三层架构,我们一般通过dao对象在每个方法中打开或关闭事务,但是我们一个service通常需要调多个dao,为了能让多个dao使用同一个数据源连接,实现跨层次的参数共享,我们每个完整的请求周期都是通过线程来处理,通过线程绑定参数,java提供的threadlocal正好解决这点。

ThreadLocal使用

  • void set(Object value)
    设置当前线程的线程局部变量值
  • public Object get()
    获取当前线程的线程局部变量值
  • public void remove()
    删除当前线程的线程局部变量值(该方法是jdk5.0新增方法,虽然对应线程结束会被垃圾回收,但是显示调用它可以加快内存的回收)
  • protected Object initialValue()
    返回对应线程的线程局部变量初始值(这个方法是一个延迟调用方法,也就是线程第一次调用get()或set()时才会执行,并且只执行一次,缺省为null)

在这里插入图片描述
贴上小源码帮助理解
在这里插入图片描述
获取的时候先获取当前线程,然后通过getMap方法得到一个ThreadLocalMap对象,它是ThreadLocal的静态内部类。
在这里插入图片描述
然后可以理解map中ThreadLocal作为键值,然后我们获取到对应的value值(其中的用数组保存了entry,因为可能有多个变量需要线程隔离访问)
其实我们的get方法就是拿到了每个线程独有的ThreadLocalMap,然后再拿当前的ThreadLocald的当前实例,拿到map中对应的entry,然后拿到对应的值返回。(如果map为空,会先进行创建初始化)

内存泄漏引发

我们先来了解一下java中的四个引用

  • 强引用
    例如“Object o = new Object();”,这就是我们很常见的强引用,在栈中的o指向堆中objecet的实例,这样的引用垃圾回收器是永远不会回收掉被引用的对象实例的。
  • 软引用
    指一些还有用但并非必需的对象。如果内存将要溢出,那么垃圾回收器就会把这些对象的实例列入回收的范围内。如果内存还不够,那么就会抛出内存溢出异常。(jdk1.2之后,提供了SoftReference类来实现软引用)
  • 弱引用
    用来描述非必需对象的,但是程度比软引用更软,就是无论当前内存是否足够,它都会被回收。(jdk1.2之后,提供WeakReference来实现)
  • 虚引用
    它地位更低,会被垃圾回收器立马回收。(jdk1.2后,提供PhantomReference来实现)

首先我们要明白,我们知道每个Thread维护一个TheadLocalMap,然后它的key是ThreadLocal,对应的value是我们需要的值。也就是ThreadLocal本身是并不储存值得,它只是作为一把钥匙供我们取值。

在这里插入图片描述
如图我们看到,ThreadLocalRef指向堆中得ThreadLocal实例,是强引用,而当前线程中有的ThreadLocalMap的Entry需要ThreadLocal作为key取value,但是因为Entry继承了WeakReference,是个弱引用,也就是这个map是使用了ThreadLocal的弱引用作为key,弱引用的对象在GC时会被回收。
在这里插入图片描述
这样如果ThreadLocal为null时,那么在ThreadLocalMap中的key中的value就会无法取到,而线程迟迟不结束,就会存在一条强引用线路:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,但是value却永远不会被访问到,所以存在内存泄漏。
只有当Thread结束,currentThread就不会存在栈中,那么强引用断开,GC才会全部回收。
那么为避免这种状况,我们在使用ThreadLocal变量之后,及时remove()。

但是我看可以看一下TheadLocal的源码,其中的get(),set()在某些时候,调用了expungeStaleEntry 方法来清楚Entry中key为null的value,但是并不是每次都会执行,所以会发生内存泄漏,只有remove()显示调用了expungeStaleEntry 方法。

错误使用ThreadLocal导致线程不安全

public class MyThread extends Thread{

    private static ThreadLocal<Number> threadLocal = new ThreadLocal<Number>();
    public static Number number = new Number(0);
    @Override
    public void run() {
        number.setI(number.getI()+1);
        threadLocal.set(number);
        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get().getI());
    }

    public static void main(String[] args) {
        for (int i = 0 ; i<=5;i++){
            new MyThread().start();
        }
    }

    public static class Number{

        public Number(int i){
            this.i = i;
        }

        private int i;

        public int getI() {
            return i;
        }

        public void setI(int i) {
            this.i = i;
        }
    }
}

在这里插入图片描述

为什么输出的结果一样呢?不应该每个线程都单独拥有一个ThreadLocal的变量的副本吗?
其实我们回顾一下,其中ThreadLocalMap中保存的是一个对象的引用,当其他线程对这个线程中指向的对象实例做修改时,本质上其实也影响了其他线程所持有的对象引用所指向的值,所以会输出同样的结果。6个线程中保存的都是同一个Number对象的引用。
正确的方式应该是我们要让每个线程中的ThreadLocal都拥有一个新的Number对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值