`

Java多线程基础总结四:ThreadLocal

阅读更多

  说到ThreadLocal,首先说说这个类的命名。直观上看好像是个Thread的什么亲戚,但其实它想表达的意思是线程本地变量,也就是说每 个线程自己的变量。它作为一个JDK5以后支持范型的类,主要是想利用范型把非线程安全的共享变量,封装成绑定线程的安全不共享变量。 这样的解释我想我们多半能猜出它的实现思路:把一个共享变量在每个线程使用时,初始化一个副本,并且和线程绑定。以后所有的线程对 共享变量的操作都是对线程内部那个副本,完全的线程内部变量的操作。
   要实现这样功能类的设计,主要技术点是要能把副本和线程绑定映射,程序可以安全查找到当前线程的副本,修改后安全的绑定给线程 。所以我们想到了Map的存储结构,ThreadLocal内部就是使用了线程安全的Map形式的存储把currentThread和变量副本一一映射。
   既然要把共享的变成不共享的,那么就要变量满足一个场景:变量的状态不需要共享。例如无状态的bean在多线程之间是安全的,因为 线程之间不需要同步bean的状态,用了就走(很不负责啊),想用就用。但是对于有状态的bean在线程之间则必须小心,线程A刚看到状态 是a,正想利用a做事情,线程B把bean的状态改为了b,结果做了不该做的。但是如果有状态的bean不需要共享状态,每个线程看到状态a或 者b都可以做出自己的行为,这种情况下不同步的选择就是ThreadLocal了。
   利用ThreadLocal的优势就在于根本不用担心有状态的bean为了状态的一致而牺牲性能,去使用synchronized限制只有一个线程在同一时 间做出关于bean状态的行为。而是多个线程同时根据自己持有的bean的副本的状态做出行为,这样的转变对于并发的支持是那么的不可思议 。例如一个 Dao内有个Connection的属性,当多个线程使用Dao的同一个实例时,问题就来了:多个线程用一个Connection,而且它还是有 连接,关闭等等的状态转变的,我们很敏感的想到这个属性不安全!再看这个属性,其实它是多么的想告诉线程哥哥们:我的这些状态根本 就不想共享,不要因为我的状态而不敢一起追求。线程哥哥们也郁闷:你要是有多胞胎姐妹该多好啊!这时候ThreadLocal大哥过来说:小 菜,我来搞定!你们这些线程一人一个 Connection,你想关就关,想连接就连接,再也不用抱怨说它把你的连接关了。这样Dao的实例再也 不用因为自己有个不安全的属性而自卑了。当然 ThreadLocal的思路虽然是很好的,但是官方的说法是最初的实现性能并不好,随着Map结 构和Thread.currentThread的改进,性能较之synchronized才有了明显的优势。所以要是使用的是JDK1.2,JDK1.3等等,也不要妄想麻雀变 凤凰...
  再看ThreadLocal和synchronized的本质。前者不在乎多占点空间,但是绝对的忍受不了等待;后者对等待无所谓,但是就是不喜欢浪费 空间。这也反映出了算法的一个规律:通常是使用场景决定时间和空间的比例,既省时又省地的算法多数情况下只存在于幻想之中。下面写 个简单的例子解释一下,不过个人觉得设计的例子不太好,以后有实际的启发再替换吧。

package thread;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalSample extends Thread {
	private OperationSample3 operationSample;

	public ThreadLocalSample(OperationSample3 operationSample) {
		this.operationSample = operationSample;
	}

	@Override
	public void run() {
		operationSample.printAndIncrementNum();
	}

	public static void main(String[] args) {

		final OperationSample3 operation = new OperationSample3();// The shared
																	// Object
																	// for
																	// threads.

		for (int i = 0; i < 5; i++) {
			new ThreadLocalSample(operation).start();
		}
	}
}

class OperationSample {
	private int num;

	// public synchronized void printAndIncrementNum() {
	public void printAndIncrementNum() {
		for (int i = 0; i < 2; i++) {
			System.out.println(Thread.currentThread().getName() + "[id=" + num
					+ "]");
			num += 10;
		}
	}
}

class OperationSample2 {

	private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};

	public void printAndIncrementNum() {
		for (int i = 0; i < 2; i++) {
			int num = threadArg.get();
			threadArg.set(num + 10);
			System.out.println(Thread.currentThread().getName() + "[id=" + num
					+ "]");
		}
	}
}

class OperationSample3 {

	private static final AtomicInteger uniqueId = new AtomicInteger(0);
	private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return uniqueId.getAndIncrement();
		}
	};

	public void printAndIncrementNum() {
		for (int i = 0; i < 2; i++) {
			int num = threadArg.get();
			threadArg.set(num + 10);
			System.out.println(Thread.currentThread().getName() + "[id=" + num
					+ "]");
		}
	}
}

这个例子中ThreadLocalSample继承自Thread持有OperationSample三个版本中的一个引用,并且在线程运行时执行 printAndIncrementNum()方法。
首先看版本1:
OperationSample有个共享变量num,printAndIncrementNum()方法没有同步保护,方法就是循环给 num赋新值并打印改变 值的线程名。因为没有任何的同步保护,所以原本打算每个线程打印出的值是相邻递加10的结果变成了不确定的递加。有可能线程1的循环 第一次打印0,第二次就打印50。这时候我们使用被注释的方法声明,结果就是预想的同一个线程的两次结果是相邻的递加,因为同一时刻 只有一个线程获得 OperationSample实例的隐式锁完成循环释放锁。
再看版本2:
假设我们有个递增10的简单计数器,但是是对每个线程的计数。也就是说我们有一个Integer计数器负责每个线程的计数。 虽然它是有状态的,会变的,但是因为每个线程之间不需要共享变化,所以可以用ThreadLocal管理这个Integer。在这里看到我们的 ThreadLocal变量的initialValue()方法被覆写了,这个方法的作用就是当调用ThreadLocal的get()获取线程绑定的副本时如果还没绑定则 调用这个方法在Map中添加当前线程的绑定映射。这里我们返回0,表示每个线程的初始副本在ThreadLocal的Map的纪录都是0。再看 printAndIncrementNum()方法,没有任何的同步保护,所以多个线程可以同时进入。但是,每个线程通过threadArg.get() 拿到的仅仅是自 己的Integer副本,threadArg.set(num + 10)的也是自己的副本值。所以结果就是虽然线程的两次循环打印有快有慢,但是每个线程的两次 结果都是0和10。
最后是版本3:
和版本2的不同在于新加了一个uniqueId的变量。这个变量是java.util.concurrent.atomic包下的原子变量类。这是基于 硬件支持的CAS(比较交换)原语的实现,所以保证了++,--,+=,-=等操作的原子性。所以在ThreadLocal变量的 initialValue()方法中使用 uniqueId.getAndIncrement()将为每个线程初始化唯一不会重复的递加1的Integer 副本值。而结果就会变成5个线程的首次打印是0~4的5 个数字,第二次每个线程的打印是线程对应的首次数字加10的值。
对于ThreadLocal的使用,Spring的源码中有大量的应用,主要是要支持Singleton的实例管理,那么自身的一些Singleton的实现内非线 程安全的变量,属性要用ThreadLocal隔离共享。同时我们在使用Spring的IOC时也要注意有可能多线程调用的注册到IOC容器的 Singleton 型实例是否真的线程安全。另外java.util.concurrent.atomic内的原子变量类简单的提了一下,再看看怎么能瞎编出东西来吧。
分享到:
评论

相关推荐

    Java多线程 之 临界区、ThreadLocal.docx

    Java多线程编程中,临界区和ThreadLocal是两种重要的并发控制机制,它们用于解决多线程环境下的数据安全问题。 1. **临界区(Critical Section)** 临界区是指一段代码,它在同一时刻只允许一个线程进行访问。在...

    java ThreadLocal多线程专属的变量源码

    java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多...

    Java多线程编程总结

    ### Java多线程编程总结 #### 一、Java线程:概念与原理 1. **操作系统中线程和进程的概念** - 当前的操作系统通常为多任务操作系统,多线程是实现多任务的一种手段。 - **进程**:指内存中运行的应用程序,每个...

    Java多线程的总结

    Java多线程是Java编程中的一个核心概念,它在现代软件开发中扮演着至关重要的角色。多线程允许程序同时执行多个任务,提高了系统资源的利用率,提升了应用程序的响应速度和并发性能。对于大型分布式系统、Web应用...

    java多线程的讲解和实战

    Java多线程是Java编程中的重要概念,尤其在如今的多核处理器环境下,理解并熟练掌握多线程技术对于提高程序性能和响应速度至关重要。本资料详细讲解了Java多线程的原理,并提供了丰富的实战代码,非常适合Java初学者...

    java多线程编程总结

    ### Java多线程编程总结 #### 一、Java线程:概念与原理 - **操作系统中线程和进程的概念** 当前的操作系统通常都是多任务操作系统,多线程是一种实现多任务的方式之一。在操作系统层面,进程指的是内存中运行的...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为核心篇和设计模式篇,旨在帮助开发者掌握并应用多线程技术。 1. **线程基础** - **线程的创建**:Java提供了两种创建线程...

    ThreadLocal相关

    在多线程环境下,ThreadLocal 变量可以保证各个线程的变量相对独立于其他线程内的变量。这种机制可以帮助开发者在多线程环境下编写更加简洁、可维护的代码。 ThreadLocal 的作用 ThreadLocal 的主要作用是提供线程...

    Java ThreadLocal详解_动力节点Java学院整理

    ThreadLocal的出现是为了解决多线程编程中的线程安全问题。 从本质上说,ThreadLocal是一种存储机制,它可以在每个线程中存储一个变量的副本,这样每个线程都可以访问自己的变量副本,而不需要与其他线程共享同一...

    我总结的Java多线程程序设计

    Java多线程程序设计是Java开发中的重要组成部分,它允许程序在同一时间执行多个任务,从而提高了系统的效率和响应性。本文将深入探讨Java多线程的相关概念和实现方式。 一、理解多线程 1. **线程定义**:线程是一...

    java多线程设计

    总结,Java多线程设计是构建高性能、高并发应用的基础。通过理解并合理使用不可变对象,我们可以有效预防多线程环境中的非安全问题,确保程序的稳定性和正确性。在实际开发中,结合各种线程同步机制和并发工具,可以...

    JAVA-多线程 所有文件

    这个“JAVA-多线程 所有文件”压缩包很可能包含了一系列关于Java多线程学习的源代码示例和相关文档。下面我们将深入探讨Java多线程的相关知识点。 1. **线程的概念**:线程是操作系统分配CPU时间的基本单位,一个...

    【JAVA多线程】多线程编程核心技术学习资料

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,极大地提高了程序的...通过阅读这本书,开发者可以深入理解Java多线程编程的核心概念,提升自己的编程能力,为应对复杂并发场景打下坚实基础。

    java线程详解

    Java线程:概念与原理 Java线程:创建与启动 Java线程:线程状态的转换 Java线程:线程的同步与锁 一、同步问题提出 二、同步和锁定 三、静态方法同步 四、如果线程不能不能获得锁会怎么样 ...Java线程:大总结

    JAVA多线程的一个带UI界面的例子

    在Java编程中,多线程是一项关键特性,...总之,这个"JAVA多线程的一个带UI界面的例子"涵盖了Java多线程编程和GUI设计的核心概念,通过实际的代码示例,有助于开发者深入理解如何在实际应用中正确、高效地使用多线程。

    java多线程编程实例_Source

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,提升系统效率。在本实例源码中,包含17个章节和上百个实例,旨在深入讲解Java多线程的核心概念和实际应用。 一、线程基础知识 在Java中,...

    Java多线程运算集合

    ### Java多线程运算集合知识点解析 #### 一、Java多线程概念与原理 - **操作系统中的线程与进程**: - **进程**:指的是一个正在运行的应用程序,每个进程都拥有独立的内存空间。 - **线程**:是进程中的一个...

    Java多线程实例代码

    Java多线程是Java编程中的重要概念,它允许程序同时执行多个任务,提高了程序的效率和响应性。在Java中,实现多线程有两种主要方式:通过继承`Thread`类和实现`Runnable`接口。 1. 继承Thread类: 当你需要创建一...

    Java多线程文档

    这篇"Java多线程文档"涵盖了关于Java多线程编程的重要知识点,以下是对这些内容的详细阐述: 一、线程的概念 线程是操作系统分配CPU时间的基本单元,一个进程中可以包含多个线程。Java中的线程有两种创建方式:通过...

Global site tag (gtag.js) - Google Analytics