ThreadLocal详解

假如我们需要自己实现一个数据库连接池,我们不能来一个线程就创建一个连接,就牵扯到多个线程竞争有限的连接数,一般会想到如下两种方案:

  • 用CAS自旋(线程请求过多导致性能下降)
  • synchronized(对象锁属于重量级锁)

如果使用了上面两种,如果写公共方法每个方法都需要传入一个连接,这样不能保证获取的连接还是上次那个,这样就保证不了事务,我们可以用Java中Thread的threadLocals属性,这样每个线程都是访问自己内部的属性,基本上避免了线程竞争带来的问题,看下threadLocals属性是什么

ThreadLocal详解_第1张图片

 threadLocals属性对应的类是属于ThreadLocal的一个静态内部类

一、ThreadLocal使用

最常用的主要是四个方法

ThreadLocal详解_第2张图片

 使用示例:

public class ThreadLocalTest {

    static ThreadLocal threadLocal = new ThreadLocal<>();
    static ThreadLocal threadLocal2 = new ThreadLocal<>();

    /**
     * 运行3个线程,每个线程持有自己独有的String类型编号
     */
    public void StartThreadArray(){
        Thread[] runs = new Thread[3];
        for(int i=0;i

 打印结果:

Thread-0:线程_0
Thread-1:线程_1
Thread-2:线程_2
Thread-0:null
Thread-2:2
Thread-1:null

ThreaLocal给每个线程都提供了一个变量的副本

ThreadLocal详解_第3张图片

二、ThreadLocal分析

1、我们自己实现和jdk帮我们实现的区别

public class MyThreadLocal {
    /*存放变量副本的map容器,以Thread为键,变量副本为value*/
    private Map threadTMap = new HashMap<>();

    public synchronized T get(){
        return  threadTMap.get(Thread.currentThread());
    }

    public synchronized void set(T t){
        threadTMap.put(Thread.currentThread(),t);
    }

}

为什么要加synchronized关键字,我们都知道HashMap底层是一个数组,每个元素的位置通过key计算hash值,根据数组长度取余就是元素所处的位置,但是hash冲突时就会在数组位置纵向生成一个链表,为防止链表过长,jdk1.8超过固定阈值生成红黑树;回到刚刚这个话题,为什么要加synchronized关键字,我的想法是为了防止hash冲突时发生覆盖,其实这种实现方式是有性能问题的,毕竟存在线程竞争关系

2、分析

上面讲到Thread类中有ThreadLocalMap属性,而ThreadLocalMap是ThreadLocal的内部类

ThreadLocal详解_第4张图片

ThreadLocal详解_第5张图片

ThreadLocal详解_第6张图片

 先获取到线程t的ThreadLocalMap属性,ThreadLocalMap具体是在Thread类的init()方法创建,最关键的还是ThreadLocalMap类,下面看看

ThreadLocal详解_第7张图片

 这里的Entry数组是存了所有线程的存储key和value,之前的ThreadLocalMap.getEntry(ThreadLocal)就是获取key和value,Entry数组里面存放多个变量的副本

3、hash冲突的解决

Hash就是把任意长度的输入(又叫做预映射,pre-image),通过散列算法,变换成固定长度的输出,常用的hash消息算法有:

  1. MD4
  2. MD5它对输入仍以512位分组,其输出是4个32位字的级联
  3. SHA-1及其它

Hash转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。比如有10000个数放到100个桶里,不管怎么放,有个桶里数字个数一定是大于2的。

所以Hash简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。常用HASH函数:直接取余法、乘法取整法、平方取中法。 Java里的HashMap用的就是直接取余法。

我们已经知道Hash属于压缩映射,一定能会产生多个实际值映射为一个Hash值的情况,这就产生了冲突,常见处理Hash冲突方法:

开放定址法

基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不同又可以分为线性探测再散列、二次探测再散列、伪随机探测再散列。

线性探测再散列即依次向后查找,二次探测再散列,即依次向前后查找,增量为1、2、3的二次方,伪随机,顾名思义就是随机产生一个增量位移。

ThreadLocal里用的则是线性探测再散列

ThreadLocal详解_第8张图片

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。Java里的HashMap用的就是链地址法,为了避免hash 洪水攻击,1.8版本开始还引入了红黑树

再哈希法

这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

三、ThreadLocal内存泄漏场景

如果我们在使用ThreadLocal时把它定义在方法栈里,方法一结束内存释放,threadLocal引用消失,而Entry数组的单个元素的key的引用也会随着gc消失,创建的ThreadLocal对象就会被回收,这样就会导致内存泄露

反例:

public class ThreadLocalMemoryLeak {
	
	private static final int TASK_LOOP_SIZE = 500;
	
	// 创建线程池
	final static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque());
	
	static class LocalVariable {
		private byte[] a = new byte[1024*1024*5]; // 5M大小的数组
	}
	
	ThreadLocal threadLocal;
	
	public static void main(String[] args) throws InterruptedException {
		Thread.sleep(4000);
		for (int i = 0;i < TASK_LOOP_SIZE; i++) {
			threadPoolExecutor.execute(new Runnable() {
				
				@Override
				public void run() {
					try {
						Thread.sleep(500);
						LocalVariable localVariable = new LocalVariable();
	                    ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
	                    oom.threadLocal = new ThreadLocal<>();
	                    oom.threadLocal.set(new LocalVariable());
	
	                    // oom.threadLocal.remove();
						System.out.println("user local variable");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			Thread.sleep(100);
		}
		System.out.println("pool execute over");
	}

}

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

public class ThreadLocalUnsafe implements Runnable {
	
	public static Number number = new Number(0);
	
	public static ThreadLocal value = new ThreadLocal();

	private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

	@Override
	public void run() {
		try {
			Random random = new Random();
			number.setNum(number.getNum()+random.nextInt(100));
			value.set(number);
			Thread.sleep(2);
			System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		 for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
	}
}

打印结果:

Thread-0=208
Thread-2=208
Thread-1=208
Thread-4=208
Thread-3=208

分析:

我本来想每个线程都生成不同的随机数,但事与愿违,static属性,类加载过程中就会加载好,并且只会加载一次,所以ThreadLocalMap中存的都是同一个对象副本,每次设置只是发生覆盖,取出来还只是同一个(去掉Thread.sleep(2)会打印不同,但指向的还是同一个对象副本,只是为了让线程一起打印ThreadLocal里面ThreadLocalMap存的副本值)

你可能感兴趣的:(并发,java,开发语言)