java之ThreadLocal详解

一、ThreadLocal简介

ThreadLocal是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问,通常是类中的private static字段。

我们知道有时候一个对象的变量会被多个线程所访问,这时就会有线程安全问题,当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

二、ThreadLocal源码分析

(1)ThreadLocal方法

ThreadLocal 的几个方法: ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的

(2)ThreadLocal实现原理

1.内部结构

Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, 在Thread类中有一个threadLocals 变量,是ThreadLocalMap类型的,它就是为每一个线程来存储自身的ThreadLocal变量的。

ThreadLocal.ThreadLocalMap threadLocals = null;

 ThreadLocalMap 是定义在ThreadLocal 类里的内部类,它的作用是存储线程的局部变量。ThreadLocalMap 以ThreadLocal的引用作为键,以局部变量作为值,存储在ThreadLocalMap.Entry (一种存储键值的数据结构)里。这是因为在每一个线程里面,可能存在着多个ThreadLocal变量

static class ThreadLocalMap {
 static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

2.调用过程

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找

注意:

在进行get之前,必须先set,否则会报空指针异常;

如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
  

ThreadLocal的set(T value)方法

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的set(ThreadLocal<?> key,Object value)

    private void set(ThreadLocal<?> key, Object value) {
 Entry[] tab = table;
 int len = tab.length;
    // Hash 寻址,与table数组长度减1(二进制全是1)相与,所以数组长度必须为2的次方,减小hash重复的可能性
 int i = key.threadLocalHashCode & (len-1);

   //从hash值计算出的下标开始遍历
 for (Entry e = tab[i];
      e != null;
      e = tab[i = nextIndex(i, len)]) {
   //获得该Entry的键
   ThreadLocal<?> k = e.get();
    //如果键和传过来的相同,覆盖原值,也说明,一个ThreadLocal变量只能为一个线程保存一个局部变量
   if (k == key) {
     e.value = value;
     return;
   }
   // 键为空,则替换该节点
   if (k == null) {
     replaceStaleEntry(key, value, i);
     return;
   }
 }

 tab[i] = new Entry(key, value);
 int sz = ++size;
   //是否需要扩容
 if (!cleanSomeSlots(i, sz) && sz >= threshold)
   rehash();
}

可以看出ThreadLocalMap 采用线性探测再散列解决Hash冲突的问题。即,如果一次Hash计算出来的数组下标被占用,即hash值重复了,则在该下标的基础上加1测试下一个下标,直到找到空值。比如说,Hash计算出来下标i为6,table[6] 已经有值了,那么就尝试table[7]是否被占用,依次类推,直到找到空值。以上,就是保存线程本地变量的方法。

TheadLocal的get()方法

public T get() {
  //获得当前线程
Thread t = Thread.currentThread();
  //得到当前线程的一个threadLocals 变量
ThreadLocalMap map = getMap(t);
if (map != null) {
  // 如果不为空,以当前ThreadLocal为主键获得对应的Entry
  ThreadLocalMap.Entry e = map.getEntry(this);
  if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
  }
}
  //如果值为空,则进行初始化
return setInitialValue();
}

ThreadLocal的setInitialValue()方法

private T setInitialValue() {
  //获得初始默认值
T value = initialValue();
  //得到当前线程
Thread t = Thread.currentThread();
  // 获得该线程的ThreadLocalMap引用
ThreadLocalMap map = getMap(t);
  //不为空则覆盖
if (map != null)
    map.set(this, value);
else
      //若是为空,则进行初始化,键为本ThreadLocal变量,值为默认值
    createMap(t, value);
}

// 默认初始化返回null值,这也是 下面demo 为什么需要重写该方法的原因。如果没有重写,第一次get()操作获得的线程本地变量为null,需要进行判断并手动调用set()进行初始化
protected T initialValue() {
    return null;
}

三、Demo

下面是个ThreadLocal使用的实例,两个任务共享同一个变量,并且两个任务都把该变量设置为了线程私有变量,这样,虽然两个任务都”持有“同一变量,但各自持有该变量的拷贝。因此,当一个线程修改该变量时,不会影响另一线程该变量的值。

package thread.ThreadLocalTest;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Created by StoneGeek on 2018/8/1.
* 博客地址:http://www.cnblogs.com/sxkgeek
*/
public class ThreadLocalDemo2 implements Runnable {
    // 一般会把 ThreadLocal 设置为static 。它只是个为线程设置局部变量的入口,多个线程只需要一个入口
    private static ThreadLocal<Student> localStudent = new ThreadLocal() {
    // 一般会重写初始化方法,一会分析源码时候会解释为什么
        @Override
        public Student initialValue() {
            return new Student();
    }
};

private Student student = null;

@Override
public void run() {
    String threadName = Thread.currentThread().getName();

    System.out.println("【" + threadName + "】:is running !");

    Random ramdom = new Random();
    //随机生成一个变量
    int age = ramdom.nextInt(100);

    System.out.println("【" + threadName + "】:set age to :" + age);
    // 获得线程局部变量,改变属性值
    Student stu = getStudent();
    stu.setAge(age);

    System.out.println("【" + threadName + "】:第一次读到的age值为 :" + stu.getAge());

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("【" + threadName + "】:第二次读到的age值为 :" + stu.getAge());
}

public Student getStudent() {
    student = localStudent.get();

//         如果不重写初始化方法,则需要判断是否为空,然后手动为ThreadLocal赋值,否则的话会报空指针异常
//        if(student == null){
//            student = new Student();
//            localStudent.set(student);
//        }

    return student;
}

public static void main(String[] args) {
    ThreadLocalDemo2 ll = new ThreadLocalDemo2();
    Thread t1 = new Thread(ll, "线程1");
    Thread t2 = new Thread(ll, "线程2");

    t1.start();
    t2.start();
}
}

console打印:
【线程2】:is running !
【线程1】:is running !
【线程1】:set age to :67
【线程2】:set age to :4
【线程1】:第一次读到的age值为 :67
【线程2】:第一次读到的age值为 :4
【线程1】:第二次读到的age值为 :67
【线程2】:第二次读到的age值为 :4

四、应用场景

最常见的ThreadLocal使用场景为
用来解决 数据库连接、Session管理等。

数据库连接

Class A implements Runnable{
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
       return connectionHolder.get();
}
}

Session管理

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

原文地址:https://www.cnblogs.com/sxkgeek/p/9406463.html

时间: 2024-09-30 01:22:33

java之ThreadLocal详解的相关文章

Java多线程编程详解

线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法:通过在方法声明中加入 synch

java移位运算符详解[转]

java移位运算符不外乎就这三种:<<(左移).>>(带符号右移)和>>>(无符号右移). 1. 左移运算符 左移运算符<<使指定值的所有位都左移规定的次数. 1)它的通用格式如下所示: value << num num 指定要移位值value 移动的位数. 左移的规则只记住一点:丢弃最高位,0补最低位 如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模.如对int型移动33位,实际上只移动了332=1位. 2)运算规则 按

java 代理模式详解

java 动态代理(JDK和cglib) 设计模式这东东每次看到就明白可过段时间又不能很流利的说出来,今天就用详细的比喻和实例来加深自己的理解(小弟水平不高有不对的地方希望大家能指出来). (1)代理这个词生活中有很多比如在街边卖手机卡.充公交地铁卡的小商店他们都起了代理的作用,java中的代理跟这些小店商的作用是一样的.再比如我想在淘宝上开个服装店但又没有货源怎么办,这时候我就要跟淘宝上某一卖家联系做他的代理.我跟我的商家都要卖衣服(就好比我们都继承了卖衣服的接口sellClothesInte

设计模式 - 迭代器模式(iterator pattern) Java 迭代器(Iterator) 详解

迭代器模式(iterator pattern) Java 迭代器(Iterator) 详解 本文地址: http://blog.csdn.net/caroline_wendy 参考迭代器模式(iterator pattern): http://blog.csdn.net/caroline_wendy/article/details/35254643 Java的标准库(util)中包含迭代器接口(iterator interface), import java.util.Iterator; 继承(

java内存泄露详解

很多人有疑问,java有很好的垃圾回收机制,怎么会有内存泄露?其实是有的,那么何为内存泄露?在Java中所谓内存泄露就是指在程序运行的过程中产生了一些对象,当不需要这些对象时,他们却没有被垃圾回收掉,而且程序运行中很难发现这个对象,它始终占据着内存却没有发挥作用. 我举这样一个例子,在现实开发中我们需要自定义一个先进后出的栈集合,代码如下: 这个代码看起来和运行起来都没问题,但是,这里有个很隐晦的问题,就是在pop()方法里面,我们首先找到集合最后一个元素的下标,然后按照下标从集合中取出,但是这

java移位运算符详解

java移位运算符不外乎就这三种:<<(左移).>>(带符号右移)和>>>(无符号右移).1.左移运算符左移运算符<<使指定值的所有位都左移规定的次数.1)它的通用格式如下所示:value << numnum 指定要移位值value 移动的位数.左移的规则只记住一点:丢弃最高位,0补最低位如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模.如对int型移动33位,实际上只移动了332=1位. 2)运算规则按二进制形式把所有的

Java关键字synchronized详解

Java关键字synchronized详解 博客分类: Java综合 Java多线程thread互联网制造 synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行 它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法: 通过在方法声明中

转:Java HashMap实现详解

Java HashMap实现详解 转:http://beyond99.blog.51cto.com/1469451/429789 1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 2.    HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造

Java网络编程详解

Java网络编程详解 http://blog.csdn.net/he90227/article/details/39184247 Java网络编程详解