提升--10---ThreadLocal简介

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--10---ThreadLocal简介相关的知识,希望对你有一定的参考价值。


ThreadLocal 简介

概念

1. ThreadLocal类

  • ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

2.ThreadLocal静态类部类----ThreadLocalMap

3.Thread类中属性threadLocals:

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

ThreadLocal是线程Thread中属性threadLocals的管理者。

举个栗子

  1. 每个人都一张银行卡
  2. 每个人每张卡都有一定的余额。
  3. 每个人获取银行卡余额都必须通过该银行的管理系统。
  4. 每个人都只能获取自己卡持有的余额信息,他人的不可访问。


作用:

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离

  1. ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,
  2. 在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。防止自己的变量被其它线程篡改。

用法案例:

  • 启动了 3 个线程,每个线程都从ThreadLocal获取数据, 并做 n += 1;
public class TestThreadLocal {

    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }


}

  • 打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15

ThreadLocal 源码解析

ThreadLocal源码的set方法

1.我们先来看一个ThreadLocal源码的set方法,ThreadLocal往里边设置值的时候是怎么设置的呢?

  1. 首先拿到当前线程,
  2. 这是你会发现,这个set方法里多了一个容器ThreadLocalMap,这个容器是一个map,是一个key/value对,然后再往下读你会发现,其实这个值是设置到了map里面,
  3. 而且这个map是什么样的,key设置的是this,value设置的是我们想要的那个值,这个this就是当前对象ThreadLocal,value就是Person类,这么理解就行了,
  4. 如果map不等于空的情况下就设置进去就行了,如果等于空呢?就创建一个map

2.我们回过头来看这个map,ThreadLocalMap map=getMap(t),我们来看看这个map到底在哪里,我们点击到了getMap这个方法看到,它的返回值是t.threadLocals

3.我们进入这个t.threadLocals,你会发现ThreadLocalMap这个东西在哪里呢?居然是在Thread这个类里,所以说这个map是在Thred类里的

这个时候我们应该明白,map的set方法其实就是设置当前线程里面的map:

  • 所以这个时候你会发现,原来THREAD_LOCAL_NUM 被set到了,当前线程里的某一个map里面去了,这个时候,我们是不是就能想明白了,我set了一个值以后,为什么其他线程访问不到?
  • 我们注重“当前线程”这个段话,所以个t1线程set了一个THREAD_LOCAL_NUM 对象到自己的map里,t2线程去访问的也是自己的属于t2线程的map,所以是读不到值的,因此你使用ThreadLocal的时候,你用set和get就完全的把他隔离开了,就是我自己线程里面所特有的,其它的线程是没有的

ThreadLocal 常见使用场景

场景1:工具类----SimpleDateFormat和Random

  • 每个线程需要一个独享的对象(通常是工具类,典工具类型需要使用的类有SimpleDateFormat和Random)

SimpleDateFormat

  • 这里1000个线程即便使用了线程池,但是每个线程都会在执行过程中创建一个SimpleDateFormat对象,这比较耗费内存资源。
  • 将SimpleDateFormat提出来用static修饰,这样每个线程都可以公用一个SimpleDateFormat对象,减少内存消耗,但是这样会打印出相同的时间,所有线程都在争夺这个资源,我们需要一个锁去控制,避免出现线程安全问题。
  • 这虽然能够满足要求,但是在高并发场景下,所有线程需要一个个的去获取锁,需要排队等待,这显然性能损耗太大。

改进

  • 使用ThreadLocal(不仅线程安全,而且也没有synchronized带来的性能问题,每个线程内有自己独享的SimpleDateFormat对象)
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalTest1 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = dateFormat(finalI);
                    System.out.println(Thread.currentThread().getName()+": "+date);
                }
            });
        }
        threadPool.shutdown();
    }

    public static String dateFormat(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); // 拿到initialValue返回对象
        return dateFormat.format(date);
    }

}



class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    // lambda表达式写法,和上面写法效果完全一样
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

场景 2:存储用户Session

很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。

每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

  1. 当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1(), service-2(), service-3(),
    service-4(),同时,每个service()都需要获得调用方用户user的信息,也就是需要拿到user对象。
  2. 一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗余且不易维护。
  3. 在此基础上可以演进,使用UserMap,就是每个用户的信息都存在一个Map中,当多线程同时工作时,我们需要保证线程安全,可以用synchronized也可以用ConcurrentHashMap,但这两者无论用什么,都会对性能有所影响。

改进

public class ThreadLocalTest2 {
    public static void main(String[] args) {
        new Service1().process("");
    }
}

class Service1 {
    public void process(String name) {
        User user = new User("张三");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}
class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>(); // 对比上一个例子,这里没有重写initialValue方法
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

  • 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。

场景 3: Spring实现事务隔离级别的源码

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

我们根据Spirng的声明式事务来解析,为什么要用ThreadLocal,声明式事务一般来讲我们是要通过数据库的,但是我们知道Spring结合Mybatis,我们是可以把整个事务写在配置文件中的,而这个配置文件里的事务,它实际上是管理了一系列的方法,方法1、方法2、方法3…,而这些方法里面可能写了,比方说第1个方法写了去配置文件里拿到数据库连接Connection,第2个、第3个都是一样去拿数据库连接,然后声明式事务可以把这几个方法合在一起,视为一个完整的事务,如果说在这些方法里,每一个方法拿的连接,它拿的不是同一个对象,你觉的这个东西能形成一个完整的事务吗?Connection会放到一个连接池里边,如果第1个方法拿的是第1个Connection,第2个拿的是第2个,第3个拿的是第3个,这东西能形成一个完整的事务吗?百分之一万的不可能,没听说过不同的Connection还能形成一个完整的事务的,那么怎么保证这么多Connection之间保证是同一个Connection呢?

把这个数据库连接 Connection放到这个线程的本地对象里ThreadLocal里面,以后再拿的时候,实际上我是从ThreadLocal里拿的,第1个方法拿的时候就把Connection放到ThreadLocal里面,后面的方法要拿的时候,从ThreadLocal里直接拿,不从线程池拿。

源码

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");

	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<>("Current transaction name");

  ……

  • 在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

如果我想共享线程的ThreadLocal数据怎么办?

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

    private void test() {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("帅得!!!!!!");
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                Log.i( "张三帅么 =" + threadLocal.get());
            }
        };
        t.start();
    }

ThreadLocal如何避免内存泄漏?

什么叫内存泄漏,对应的什么叫内存溢出

  1. Memory overflow:内存溢出 OOM
    没有足够的内存提供申请者使用。
  2. Memory leak:内存泄漏,
    程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。

显然是TreadLocal在不规范使用的情况下导致了内存没有释放。

分析;

public class T03_WeakReference {
    public static void main(String[] args) {
        WeakReference<M> m = new WeakReference<>(new M());
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
        
        ThreadLocal<M> tl = new ThreadLocal<>();
        tl.set(new M());
        tl.remove();
    }
}

  1. 我们来看上面的图,从左往右看,首先我们来说当前肯定是有一个线程的,任何一个方法肯定是要运行在某个线程里的,这个线程是我的主线程,在这个线程里有一个线程的局部变量叫tl,tl它new出来了一个ThreadLoal对象,这是一个强引用没问题,
  2. 然后我又往ThreadLocal里放了一个对象,可是你们是不是还记得,往ThreadLocal里放对象的话,实际上是放到了当前线程的一个threadLocals变量里面,这个threadLocals变量指向的是一个Map,也就是我们把这个M对象给放到了这Map里面,它的key是我们的ThreadLocal对象,value是我们的M对象,
  3. 我们来回想一下,往ThreadLocal里面set的时候,先拿到当前线程,然后拿到当前线程里面的那个Map,然后通过这个Map把ThreadLocal对象给set进去,这个map.set(this, value)方法中的this是谁?是ThreadLocal对象,set进去的时候往里面放了这么一个东西叫Entry,这个Entry又是什么呢?注意看代码,这个Entry是从弱引用WeakReference继承出来的

static class Entry extends WeakReference<ThreadLocal<?>>

  1. 这时候我们应该明白了,这里tl是一个强引用指向这个ThreadLocal对象,而Map里的key是通过一个弱引用指向了一个ThreadLocal对象
  2. 我们假设这是个强引用,当tl指向这个ThreadLocal对象消失的时候,tl这个东西是个局部变量,方法已结束它就消失了,当tl消失了,如果这个ThreadLocal对象还被一个强引用的key指向的时候,这个ThreadLocal对象能被回收吗?肯定不行,而且由于这个线程有很多线程是长期存在的,比如这个是一个服务器线程,7*24小时一年365天不间断运行,那么不间断运行的时候,这个tl会长期存在,这个Map会长期存在,这个Map的key也会长期存在,这个key长期存在的话,这个ThreadLocal对象永远不会被消失,所以这里是不是就会有内存泄漏
  3. 但是如果这个key是弱引用的话还会存在这个问题吗?当这个强引用消失的时候这个弱引用是不是自动就会回收了,这也是为什么用WeakReference的原因

隐患问题:

关于ThreadLocal还有一个问题,当我们tl这个强引用消失了,key的指向也被回收了,可是很不幸的是这个key指向了一个null值,但是这个threadLocals的Map是永远存在的,相当于说key/value对,你这个key是null的,你这个value指向的东西,你的这个10MB的字节码,你还能访问到吗?访问不到了,如果这个Map越积攒越多,越来越多,它还是会内存泄漏,怎么办呢?所以必须记住这一点,使用ThreadLocal里面的对象不用了,务必要remove掉,不然还会有内存泄漏

使用ThreadLocal里面的对象不用了,务必要remove掉,不然还会有内存泄漏

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

以上是关于提升--10---ThreadLocal简介的主要内容,如果未能解决你的问题,请参考以下文章

Python Qt GUI设计:QTableViewQListViewQListWidetQTableWidgetQTreeWidget和QTreeWidgetltem表格和树类(提升篇—1)(代码片

嵌入式开发裸机引导操作系统和ARM 内存操作 ( DRAM SRAM 类型 简介 | Logical Bank | 内存地址空间介绍 | 内存芯片连接方式 | 内存初始化 | 汇编代码示例 )(代码片

Java多线程10:ThreadLocal的作用及使用

10.ThreadLocal

错误记录Oboe / AAudio 播放器报错 ( onEventFromServer - AAUDIO_SERVICE_EVENT_DISCONNECTED - FIFO cleared )(代码片

SQLServer 2014 内存优化表