`

Java 多线程同步问题的探究(五、你有我有全都有—— ThreadLocal如何解决并发安全性?)【更新重要补疑】

 
阅读更多

前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。

在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?

什么是本地线程?
本地线程开玩笑的说:不要迷恋哥,哥只是个传说。

其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实 现。

当使用ThreadLocal 维护变量时,它会为每个使用该变量的线程提供独立的变量副本 。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线 程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消 耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。

说了这么多,来看看如何正确使用ThreadLocal。

通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。

还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。


下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:

package sky.cn.test4;

import java.util.Random;

public class ThreadDemo3 extends Thread {
	private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>();
	
	public ThreadDemo3(Student stu) {
		stuLocal.set(stu);
	}
	
	public static void main(String[] args) {
		Student stu = new Student();
		ThreadDemo3 td31 = new ThreadDemo3(stu);
		ThreadDemo3 td32 = new ThreadDemo3(stu);
		ThreadDemo3 td33 = new ThreadDemo3(stu);
		td31.start();
		td32.start();
		td33.start();
	}
	
	public void run() {
		accessStudent();
	}
	
	public void accessStudent() {
		String currentThreadName = Thread.currentThread().getName();
		System.out.println(currentThreadName + " is running!!");
		Random random = new Random();
		int age = random.nextInt(100);
		System.out.println("Thread " + currentThreadName +
				" set age to: " + age);
		Student student = stuLocal.get();
		
		student.setAge(age);
		System.out.println("Thread " + currentThreadName +
				" first read age is: " + student.getAge());
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("Thread " + currentThreadName + 
				" second read age is: " + student.getAge());
	}
}

而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在 当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为 key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

 

 

 

这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入 Student对象,而此时,以ThreadLocal对象为key,Student对象 为value的Map是被放入 当前的活动线程内的。也就是 Main线程 。而当我们的3个ThreadDemo3线程运行起来 以后,调用get()方法 ,都是试图从当前的活动线程中取 得 ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程 了,于是,程序最终执行了ThreadLocal原生的 initialValue() 方法,返回了null。

 

讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。

 

于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。


这样,我们就有了下面的正确例程:

 

package sky.cn.test4;

import java.util.Random;

public class ThreadDemo3 extends Thread {
	private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>() {
		@Override
		protected Student initialValue() {
			return new Student();
		}
	};
	
	public ThreadDemo3() {
	}
	
	public static void main(String[] args) {
		ThreadDemo3 td31 = new ThreadDemo3();
		ThreadDemo3 td32 = new ThreadDemo3();
		ThreadDemo3 td33 = new ThreadDemo3();
		td31.start();
		td32.start();
		td33.start();
	}
	
	public void run() {
		accessStudent();
	}
	
	public void accessStudent() {
		String currentThreadName = Thread.currentThread().getName();
		System.out.println(currentThreadName + " is running!!");
		Random random = new Random();
		int age = random.nextInt(100);
		System.out.println("Thread " + currentThreadName +
				" set age to: " + age);
		Student student = stuLocal.get();
		
		student.setAge(age);
		System.out.println("Thread " + currentThreadName +
				" first read age is: " + student.getAge());
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("Thread " + currentThreadName + 
				" second read age is: " + student.getAge());
	}
}

 ********** 补疑 ******************

有的童鞋可能会问:“你这个Demo根本没体现出来,每个线程里都有一个ThreadLocal对象;应该是一个ThreadLocal对象对应多个线程,你这变成了一对一,完全没体现出ThreadLocal的作用。”

那么我们来看一下如何用一个ThreadLocal对象来对应多个线程:

 

package sky.cn.test4;

import java.util.Random;

public class ThreadDemo3 implements Runnable {
	private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>() {
		@Override
		protected Student initialValue() {
			return new Student();
		}
	};
	
	public ThreadDemo3() {
	}
	
	public static void main(String[] args) {
		ThreadDemo3 td3 = new ThreadDemo3();
		Thread t1 = new Thread(td3);
		Thread t2 = new Thread(td3);
		Thread t3 = new Thread(td3);
		t1.start();
		t2.start();
		t3.start();
		
	}
	
	public void run() {
		accessStudent();
	}
	
	public void accessStudent() {
		String currentThreadName = Thread.currentThread().getName();
		System.out.println(currentThreadName + " is running!!");
		Random random = new Random();
		int age = random.nextInt(100);
		System.out.println("Thread " + currentThreadName +
				" set age to: " + age);
		Student student = stuLocal.get();
		
		student.setAge(age);
		System.out.println("Thread " + currentThreadName +
				" first read age is: " + student.getAge());
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("Thread " + currentThreadName + 
				" second read age is: " + student.getAge());
	}
}

 

这里,多个线程对象都使用同一个实现了Runnable接口的ThreadDemo3对象来构造 。这样,多个线程使用的ThreadLocal对象就是同一个 。结果仍然是正确的。但是仔细回想一下,这两种实现方案有什么不同呢?

答案其实很简单,并没有本质上的不同。对于第一种实现 ,不同的线程对象当中ThreadLocalMap里面的KEY使用的是不同的 ThreadLocal对象 。而对于第二种实现不同的线程对象当中ThreadLocalMap里面的KEY是同一个ThreadLocal对象 。但是 从本质上讲,不同的线程对象都是利用其自身的ThreadLocalMap对象来对各自的Student对象进行封装,用ThreadLocal对象作为 该ThreadLocalMap的KEY。所以说,“ThreadLocal的思想精髓就是为每个线程创建独立的资源副本。”这句话并不应当被理解成:一 定要使用同一个ThreadLocal对象来对多个线程进行处理。因为真正用来封装变量的不是ThreadLocal。就算是你的程序中所有线程都共用同 一个ThreadLocal对象,而你真正封装到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同对象。就好比线程 就是房东,ThreadLocalMap就是房东的房子。房东通过ThreadLocal这个中介去和房子里的房客打交道,而房东不管要让房客住进去还是 搬出来,都首先要经过ThreadLocal这个中介。

所以提到ThreadLocal,我们不应当顾名思义的认为JDK里面提供ThreadLocal就是提供了一个用来封装本地线程存储的容器,它本身并没 有Map那样的容器功能。真正发挥作用的是ThreadLocalMap。也就是说,事实上,采用ThreadLocal来提高并发行,首先要理解,这不 是一种简单的对象封装,而是一套机制,而这套机制中的三个关键因素(Thread、ThreadLocal、ThreadLocalMap)之间的关系是 值得我们引起注意的。

**************** 补疑完毕 ***************************

可见,要正确使用ThreadLocal,必须注意以下几点:

1. 总是对ThreadLocal中的initialValue()方法进行覆盖

2. 当使用set()或get()方法时 牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程

3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。

 

运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。

 

现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。

当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。

把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新 的思路。它为每个线程创建一个独立的资源副本 ,从而将多个线程中的数据隔离开来 ,避免了同步所产生的性能问题,是一种“以空间换时间 ”的解决方案。
但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
好了,本系列到此马上就要划上一个圆满的句号了。不知大家有什么意见和疑问没有。希望看到你们的留言。

下一讲中我们就来对之前的内容进行一个总结,顺便讨论一下被遗忘的volatile关键字。敬请期待。

 

来源: http://www.blogjava.net/zhangwei217245/archive/2010/04/24/317651.html#317694

分享到:
评论

相关推荐

    基于Java多线程同步的安全性研究.pdf

    最后,文章总结了基于Java多线程同步的安全性研究的重要性和必要性,并提出了解决Java多线程同步机制中安全性问题的方法。本文为Java多线程同步机制的研究和实现提供了有价值的参考。 知识点: 1.Java多线程同步...

    Java多线程同步问题的探究.pdf

    在探究Java多线程同步问题时,需要关注的关键知识点包括synchronized关键字的使用、JDK 5引入的java.util.concurrent.locks包下的锁机制、以及JDK 1.2中ThreadLocal类的使用。此外,了解JVM(Java虚拟机)在处理多...

    java多线程设计

    3. 线程同步:Java提供了多种同步机制,如synchronized关键字、volatile变量、Lock接口(ReentrantLock等)、Semaphore信号量、CyclicBarrier屏障等,用于解决多线程环境下资源竞争和数据一致性问题。 二、不可变...

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

    Java多线程并发访问解决方案

    在Java编程中,多线程并发访问是提升应用程序性能的重要手段,但同时也带来了数据一致性、安全性及效率等问题。本文将深入探讨Java中的多线程并发访问解决方案,主要围绕以下几个核心知识点进行阐述: 1. **线程...

    Java多线程与并发库高级应用视频教程22集

    资源名称:Java多线程与并发库高级应用视频教程22集资源目录:【】01传统线程技术回顾【】02传统定时器技术回顾【】03传统线程互斥技术【】04传统线程同步通信技术【】04传统线程同步通信技术_分割纪录【】05线程...

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

    - **同步机制**:包括`synchronized`关键字、`volatile`关键字、`Lock`接口及其子类,用于解决多线程环境中的数据一致性问题。 2. **并发控制** - **互斥与同步**:通过`synchronized`关键字实现临界区的互斥访问...

    java多线程编程实例_Source

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

    java多线程安全性基础介绍.pptx

    java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...

    Java多线程的总结

    Java集合框架中有一些线程安全的类,如Vector、HashTable、ConcurrentHashMap等,它们内部实现了同步机制,可以在多线程环境中直接使用,避免了手动同步的复杂性。 十、线程局部变量 ThreadLocal为每个线程都创建了...

    Java 多线程与并发编程总结.doc

    Java多线程与并发编程是Java开发中不可或缺的一部分,它涉及到如何高效地利用CPU资源,实现并发执行多个任务。在操作系统层面,多线程是为了提高系统利用率,使得多个任务能够"同时"执行,但实际上,由于CPU的时钟...

    Java多线程 - (一) 最简单的线程安全问题

    总之,理解并掌握Java中的线程安全问题及其解决方案是每个Java开发者必备的技能,这不仅可以确保程序的正确性,还能有效利用多核处理器,提升系统性能。在阅读源码时,也要注意观察作者如何处理线程安全,这对于提升...

    java事务 - threadlocal

    Java事务和ThreadLocal是两种在Java编程中至关重要的概念,它们分别用于处理多线程环境下的数据一致性问题和提供线程局部变量。 首先,我们来深入理解Java事务。在数据库操作中,事务是一系列操作的集合,这些操作...

    经典Java多线程与并发库高级应用

    在深入探讨Java多线程与并发库的高级应用前,有必要了解一些基础概念。Java线程是Java程序的基础,它代表程序中的一条执行线索或线路。在Java中创建线程有两种传统方式,一种是通过继承Thread类并覆盖其run方法来...

    张孝祥Java多线程与并发库高级应用笔记

    - **JDK1.5线程并发库**:引入了更高级的并发控制机制,如`ExecutorService`、`Callable`、`Future`等,提供了更安全、更高效的多线程解决方案,减少了线程同步和死锁的风险。 ##### 2. 创建线程的两种传统方式 - ...

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

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在现代计算机系统中,多线程技术尤其关键,因为它们能够充分利用多核处理器的能力。这份"Java多线程编程...

    java多线程_java多线程下变量共享_

    Java多线程是Java编程中的重要概念,它允许程序同时执行多个任务,从而提升系统效率。在多线程环境中,变量共享是一个常见的需求,但也是引发问题的关键点。本篇文章将深入探讨Java多线程下变量共享的问题以及解决...

Global site tag (gtag.js) - Google Analytics