快速了解Java中ThreadLocal类

最近看Android FrameWork层代码,看到了ThreadLocal这个类,有点儿陌生,就翻了各种相关博客一一拜读;自己随后又研究了一遍源码,发现自己的理解较之前阅读的博文有不同之处,所以决定自己写篇文章说说自己的理解,希望可以起到以下作用:
- 可以疏通研究结果,加深自己的理解;
- 可以起到抛砖引玉的作用,帮助感兴趣的同学疏通思路;
- 分享学习经历,同大家一起交流和学习。

一、 ThreadLocal 是什么

ThreadLocal 是Java类库的基础类,在包java.lang下面;

官方的解释是这样的:
Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same ThreadLocal object, but each sees a different value when accessing it, and changes made by one thread do not affect the other threads. The implementation supports null values.

大致意思是:
可以实现线程的本地存储机制,ThreadLocal变量是一个不同线程可以拥有不同值的变量。所有的线程可以共享同一个ThreadLocal对象,但是不同线程访问的时候可以取得不同的值,而且任意一个线程对它的改变不会影响其他线程。类实现是支持null值的(可以在set和get方法传递和访问null值)。

概括来讲有三个特性:

- 不同线程访问时取得不同的值
- 任意线程对它的改变不影响其他线程
- 支持null

下面分别对这些特性进行实例验证,首先定义一个Test类,在此类中我们鉴证上边所提到的三个特性。类定义如下:

Test.java

public class Test{
	//定义ThreadLocal
	private static ThreadLocal name;
	public static void main(String[] args) throws Exception{
		name = new ThreadLocal();
		//Define Thread A
		Thread a = new Thread(){
			public void run(){
				System.out.println("Before invoke set,value is:"+name.get());
				name.set(“Thread A”);
				System.out.println("After invoke set, value is:"+name.get());
			}
		}
		;
		//Define Thread B
		Thread b = new Thread(){
			public void run(){
				System.out.println("Before invoke set,value is :"+name.get());
				name.set(“Thread B”);
				System.out.println("After invoke set,value is :"+name.get());
			}
		}
		;
		// Not invoke set, print the value is null
		System.out.println(name.get());
		// Invoke set to fill a value
		name.set(“Thread Main”);
		// Start thread A
		a.start();
		a.join();
		// Print the value after changed the value by thread A
		System.out.println(name.get());
		// Start thread B
		b.start();
		b.join();
		// Print the value after changed the value by thread B
		System.out.println(name.get())
	}
}

代码分析:

从定义中我们可以看到只声明了一个ThreadLocal对象,其他三个线程(主线程、Thread A和Thread B)共享同一个对象;然后,在不同的线程中修改对象的值和在不同的线程中访问对象的值,并在控制台输出查看结果。

看结果:

从控制台输出结果可以看到里边有三个null的输出,这个是因为在输出前没有对对象进行赋值,验证了支持null的特点;再者,还可以发现在每个线程我都对对象的值做了修改,但是在其他线程访问对象时并不是修改后的值,而是访问线程本地的值;这样也验证了其他两个特点。

二、 ThreadLocal的作用

大家都知道它的使用场景大都是多线程编程,至于具体的作用,这个怎么说那?我觉得这个只能用一个泛的说法来定义,因为一个东西的功能属性定义了以后会限制大家的思路,就好比说菜刀是用来切菜的,好多人就不会用它切西瓜了。
这里,说下我对它的作用的认识,仅供参考,希望能有所帮助。这样来描述吧,当一个多线程的程序需要对多数线程的部分任务(就是run方法里的部分代码)进行封装时,在封装体里就可以用ThreadLocal来包装与线程相关的成员变量,从而保证线程访问的独占性,而且所有线程可以共享一个封装体对象;可以参考下Android里的Looper。不会用代码描述问题的程序员不是好程序员;

看代码:统计线程某段代码耗时的工具(为说明问题自造)

StatisticCostTime.java

// Class that statistic the cost time
public class StatisticCostTime{
	// record the startTime
	// private ThreadLocal startTime = new ThreadLocal();
	private long startTime;
	// private ThreadLocal costTime = new ThreadLocal();
	private long costTime;
	private StatisticCostTime(){
	}
	//Singleton
	public static final StatisticCostTime shareInstance(){
		return InstanceFactory.instance;
	}
	private static class InstanceFactory{
		private static final StatisticCostTime instance = new StatisticCostTime();
	}
	// start
	public void start(){
		// startTime.set(System. nanoTime ());
		startTime = System.nanoTime();
	}
	// end
	public void end(){
		// costTime.set(System. nanoTime () - startTime.get());
		costTime = System.nanoTime() - startTime;
	}
	public long getStartTime(){
		return startTime;
		// return startTime.get();
	}
	public long getCostTime(){
		// return costTime.get();
		return costTime;
	}

好了,工具设计完工了,现在我们用它来统计一下线程耗时试试呗:

Main.java

public class Main{
	public static void main(String[] args) throws Exception{
		// Define the thread a
		Thread a = new Thread(){
			public void run(){
				try{
					// start record time
					StatisticCostTime.shareInstance().start();
					sleep(200);
					// print the start time of A
					System.out.println("A-startTime:"+StatisticCostTime.shareInstance().getStartTime());
					// end the record
					StatisticCostTime.shareInstance().end();
					// print the costTime of A
					System.out.println("A:"+StatisticCostTime.shareInstance().getCostTime());
				}
				catch(Exception e){
				}
			}
		}
		;
		// start a
		a.start();
		// Define thread b
		Thread b = new Thread(){
			public void run(){
				try{
					// record the start time of B1
					StatisticCostTime.shareInstance().start();
					sleep(100);
					// print the start time to console
					System.out.println("B1-startTime:"+StatisticCostTime.shareInstance().getStartTime());
					// end record start time of B1
					StatisticCostTime.shareInstance().end();
					// print the cost time of B1
					System.out.println("B1:"+StatisticCostTime.shareInstance().getCostTime());
					// start record time of B2
					StatisticCostTime.shareInstance().start();
					sleep(100);
					// print start time of B2
					System.out.println("B2-startTime:"+StatisticCostTime.shareInstance().getStartTime());
					// end record time of B2
					StatisticCostTime.shareInstance().end();
					// print cost time of B2
					System.out.println("B2:"+StatisticCostTime.shareInstance().getCostTime());
				}
				catch(Exception e){
				}
			}
		}
		;
		b.start();
	}
}

运行代码后输出结果是这样的
注意:输出结果精确度为纳秒级

看结果是不是和我们预想的不一样,发现A的结果应该约等于B1+B2才对呀,怎么变成和B2一样了那?答案就是我们在定义startTime和costTime变量时,本意是不应共享的,应是线程独占的才对。而这里变量随单例共享了,所以当计算A的值时,其实startTime已经被B2修改了,所以就输出了和B2一样的结果。

现在我们把StatisticCostTime中注释掉的部分打开,换成ThreadLocal的声明方式试下。
看结果:

呀!这下达到预期效果了,这时候有同学会说这不是可以线程并发访问了吗,是不是只要我用了ThreadLocal就可以保证线程安全了?答案是no!首先先弄明白为什么会有线程安全问题,无非两种情况:
1、不该共享的资源,你在线程间共享了;
2、线程间共享的资源,你没有保证有序访问;
前者可以用“空间换时间”的方式解决,用ThreadLocal(也可以直接声明线程局部变量),后者用“时间换空间”的方式解决,显然这个就不是ThreadLocal力所能及的了。

三、 ThreadLocal 原理

实现原理其实很简单,每次对ThreadLocal 对象的读写操作其实是对线程的Values对象的读写操作;这里澄清一下,没有什么变量副本的创建,因为就没有用变量分配的内存空间来存T对象的,而是用它所在线程的Values来存T对象的;我们在线程中每次调用ThreadLocal的set方法时,实际上是将object写入线程对应Values对象的过程;调用ThreadLocal的get方法时,实际上是从线程对应Values对象取object的过程。

看源码:

ThreadLocal 的成员变量set

/**
 * Sets the value of this variable for the current thread. If set to
 * {@code null}, the value will be set to null and the underlying entry will
 * still be present.
 *
 * @param value the new value of the variable for the caller thread.
 */
public void set(T value) {
  Thread currentThread = Thread.currentThread();
  Values values = values(currentThread);
  if (values == null) {
    values = initializeValues(currentThread);
  }
  values.put(this, value);
}

TreadLocal 的成员方法get

/**
 * Returns the value of this variable for the current thread. If an entry
 * doesn't yet exist for this variable on this thread, this method will
 * create an entry, populating the value with the result of
 * {@link #initialValue()}.
 *
 * @return the current value of the variable for the calling thread.
 */
@SuppressWarnings("unchecked")
public T get() {
  // Optimized for the fast path.
  Thread currentThread = Thread.currentThread();
  Values values = values(currentThread);
  if (values != null) {
    Object[] table = values.table;
    int index = hash & values.mask;
    if (this.reference == table[index]) {
      return (T) table[index + 1];
    }
  } else {
    values = initializeValues(currentThread);
  }

  return (T) values.getAfterMiss(this);
}

ThreadLocal的成员方法initializeValues

/**
 * Creates Values instance for this thread and variable type.
 */
Values initializeValues(Thread current) {
  return current.localValues = new Values();
}

ThreadLocal 的成员方法values

/**
 * Gets Values instance for this thread and variable type.
 */
Values values(Thread current) {
  return current.localValues;
}

那这个Values又是怎样读写Object那?

Values是作为ThreadLocal的内部类存在的;这个Values里包括了一个重要数组Object[],这个数据就是解答问题的关键部分,它是用来存储线程本地各种类型TreadLocal变量用的;那么问题来了,具体取某个类型的变量时是怎么保证不取到其他类型的值那?按一般的做法会用一个Map根据key-value映射一下的;对的,思路就是这个思路,但是这里并没有用Map来实现,是用一个Object[]实现的Map机制;但是,若要用Map理解的话,也是不可以的,因为机制是相同的;key其实上对应ThreadLocal的弱引用,value就对应我们传进去的Object。

解释下是怎么用Object[]实现Map机制的(参考图1);它是用数组下标的奇偶来区分key和value的,就是下表是偶数的位置存储key,奇数存储value,就是这样搞得;感兴趣的同学如果想知道算法实现的话,可以深入研究一下,这里我不在详述了。

结合前面第一个实例分析下存储情况:

当程序执行时存在A,B和main三个线程,分别在线程中调用name.set()时同时针对三个线程实例在堆区分配了三块相同的内存空间来存储Values对象,以name引用作为key,具体的object作为值存进三个不同的Object[](参看下图):

四、 总结

ThreadLocal 不能完全解决多线程编程时的并发问题,这种问题还要根据不同的情况选择不同的解决方案,“空间换时间”还是“时间换空间”。

ThreadLocal最大的作用就是把线程共享变量转换成线程本地变量,实现线程之间的隔离。

以上就是本文关于快速了解Java中ThreadLocal的全部内容,希望对大家有所帮助。如有不足之处,欢迎留言指出。感谢朋友们对本站的支持。

(0)

相关推荐

  • Java多线程编程中ThreadLocal类的用法及深入

    ThreadLocal,直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名. 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用.其实它还是挺有用的,不相信的话,我们一起

  • Java多线程编程之ThreadLocal线程范围内的共享变量

    模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * 线程范围内的共享变量 * * 三个模块共享数据,主线程模块和AB模块 * * @author Administrator * */ public class ThreadScopeS

  • java 中ThreadLocal 的正确用法

    java 中ThreadLocal 的正确用法 用法一:在关联数据类中创建private static ThreadLocalThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread.如果我们希望通过某个类将状态(例如用户ID.事务ID)与线程关联起来,那么通常在这个类中定义private s

  • Java ThreadLocal用法实例详解

    本文实例讲述了Java ThreadLocal用法.分享给大家供大家参考,具体如下: 目录 ThreadLocal的基本使用 ThreadLocal实现原理 源码分析(基于openjdk11) get方法: setInitialValue方法 getEntry方法 set方法 ThreadLocalMap的set方法 replaceStaleEntry方法 cleanSomeSlots方法 rehash方法 expungeStaleEntries方法 resize方法 ThreadLocal实现

  • 简单分析Java线程编程中ThreadLocal类的使用

    一.概述   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.   从线程的角度看,每个线程都保持一个对

  • java线程本地变量ThreadLocal详解

    介绍 ThreadLocal作为JDK1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量 从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量 通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线

  • 深入解析Java中ThreadLocal线程类的作用和用法

    ThreadLocal与线程成员变量还有区别,ThreadLocal该类提供了线程局部变量.这个局部变量与一般的成员变量不一样,ThreadLocal的变量在被多个线程使用时候,每个线程只能拿到该变量的一个副本,这是Java API中的描述,通过阅读API源码,发现并非副本,副本什么概念?克隆品? 或者是别的样子,太模糊.   准确的说,应该是ThreadLocal类型的变量内部的注册表(Map<Thread,T>)发生了变化,但ThreadLocal类型的变量本身的确是一个,这才是本质!  

  • 实例讲解Java并发编程之ThreadLocal类

    ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值.可以将ThreadLocal<T>视为 包含了Map<Thread,T>对象,保存了特定于该线程的值. 概括起来说,对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间

  • Java 并发编程之ThreadLocal详解及实例

    Java 理解 ThreadLocal 摘要: ThreadLocal 又名线程局部变量,是 Java 中一种较为特殊的线程绑定机制,用于保证变量在不同线程间的隔离性,以方便每个线程处理自己的状态.进一步地,本文以ThreadLocal类的源码为切入点,深入分析了ThreadLocal类的作用原理,并给出应用场景和一般使用步骤. 一. 对 ThreadLocal 的理解 1). ThreadLocal 概述 ThreadLocal 又名 线程局部变量,是 Java 中一种较为特殊的 线程绑定机制

  • Java ThreadLocal类应用实战案例分析

    本文实例讲述了Java ThreadLocal类应用.分享给大家供大家参考,具体如下: 一 点睛 ThreadLocal,是Thread Local Variable(线程局部变量)的意思,也许将它命名为ThreadLocalVar更加合适. 线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.从线程的角度看,就好像每一个线程都完全拥有该变量. ThreadLocal类的

  • Java 中ThreadLocal类详解

    ThreadLocal类,代表一个线程局部变量,通过把数据放在ThreadLocal中,可以让每个线程创建一个该变量的副本.也可以看成是线程同步的另一种方式吧,通过为每个线程创建一个变量的线程本地副本,从而避免并发线程同时读写同一个变量资源时的冲突. 示例如下: import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import ja

随机推荐