多线程(六):ThreadLocal 关键字

时间:2021-12-09 17:03:46

包括:

一. 什么是 ThreadLocal

二. ThreadLocal 类中的方法简介

三. 如何使用ThreadLocal

     3.1 3个线程共享一个对象,各自产生序列号

     3.2 例子:共享变量a, 使用ThreadLocal 和没有使用的区别

四. ThreadLocal 和同步机制的比较

五.参考

一.什么是ThreadLocal

       ThreadLocal并非一个线程,而是一个线程局部变量。它的作用就是为使用该变量的线程都提供一个变量值的副本,每个线程都可以独立的改变自己的副本,而不会和其他线程的副本造成冲突。

       从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

       通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

       ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

       概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。


二.ThreadLocal类中的方法简介

2.1 get()方法:

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();

}

       返回此线程局部变量的当前线程副本中的值。首先得到该线程t,由于ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本,所以使用ThreadLocalMap map = getMap(t);方法取得当前线程副本中的值,如果这是线程第一次调用该方法,即map为空,那么调用setInitialValue()方法创建并初始化此副本。


2.2 remove()方法

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

       移除此线程局部变量当前线程的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。


三.如何使用ThreadLocal
3.1 3个线程共享一个对象,各自产生序列号

public class SequenceNumber {
	
	//通过一个匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
	private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
		public Integer initialValue(){
			return 0;
		}
	};
	
	//获取下一个序列号
	public int getNextNum(){
		seqNum.set(seqNum.get() + 1);
		return seqNum.get();
	}
	
	public static void main(String args[]){
		
		SequenceNumber sn = new SequenceNumber();
		
		ThreadClient t1 = new ThreadClient(sn);
		ThreadClient t2 = new ThreadClient(sn);
		ThreadClient t3 = new ThreadClient(sn);
		
		t1.start();
		t2.start();
		t3.start();
	}
	
	private static class ThreadClient extends Thread
	{
		private SequenceNumber sn;
		
		public ThreadClient(SequenceNumber sn)
		{
			this.sn = sn;
		}
		
		public void run(){
			for(int i = 0; i < 3; ++i)
			{
				System.out.println("Thread[" + Thread.currentThread().getName() +
						"] sn[" + sn.getNextNum() + "]");
			}
		}
	}
	
}

       那么,对于这个例子来说,首先是3个线程共享了SequenceNumber对象,那么如何才能保证用同一个东西,但是互不影响,这时候就用到了ThreadLocal来实现这个功能。ThreadLocal类中的Map,存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。所以,各个线程都是在各自的副本上做操作,互不影响。

概况说下上述代码流程:

  1. 首先在SequenceNumber类中创建ThreadLocal对象seqNum用来保存线程间需要隔离处理的对象
  2. 创建一个获取要隔离访问的数据的方法getXxx(),比如上述的public int getNextNum()方法,主要是里面的seqNum.get()方法。
  3. 在run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

       可能会有个疑问:创建3个线程,这3个线程是什么时候存到ThreadLocal的Map里面的,而且是怎么存放的,get方法取出的时候怎么知道取哪一个。

       首先解决是什么时候这3个线程存放到Map里面的,在上述代码调用public int getNextNum()中的seqNum.get()方法,我们看一下这个get方法:

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;
    }

     

       首先调用get方法的时候,先取得当前的线程t,然后看一下这个键对值map有没有,没有的话,调用setInitialValue()返回一个初始的,并且加入到map中,有的话就返回这个值。所以说,这3个线程以及他们各个的副本的值什么时候存放在Threadlocal的map中,就是在get方法调用的时候。


3.2  例子:共享变量a,使用ThreadLocal和没有使用的区别

public class Test {
	public static int a = 0;
	
	public static void main(String args[])
	{
		MyThread myThread = new MyThread();
		myThread.start();
		
		for(int i = 0; i < 5; ++i)
		{
			a = a + 1;
			System.out.println(Thread.currentThread().getName() + ":" + a);
		}
	}
	
	public static class MyThread extends Thread
	{
		public void run()
		{
			for(int i = 0; i < 5; ++i)
			{
				a = a + 1;
				System.out.println(Thread.currentThread().getName() + ":" + a);
			}
		}
	}
}

输出结果为:

main:2
main:3
main:4
main:5
main:6
Thread-0:2
Thread-0:7
Thread-0:8
Thread-0:9
Thread-0:10


在没有同步的机制下,该结果有不安全的情况发生是正常的,两个线程都得到2。那么,当使用了ThreadLocal的时候:

 public class Test {
	
	private static ThreadLocal<Integer> a = new ThreadLocal<Integer>(){
		public Integer initialValue(){
			return 0;
		}
	};
	
	public static void main(String args[])
	{
		MyThread myThread = new MyThread();
		myThread.start();
		
		for(int i = 0; i < 5; ++i)
		{
			a.set(a.get() + 1);
			System.out.println(Thread.currentThread().getName() + ":" + a.get());
		}
	}
	
	public static class MyThread extends Thread
	{
		public void run()
		{
			for(int i = 0; i < 5; ++i)
			{
				a.set(a.get() + 1);
				System.out.println(Thread.currentThread().getName() + ":" + a.get());
			}
		}
	}
}

输出结果为:

main:1
main:2
main:3
main:4
main:5
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5

再次分析下如何使用使用TheadLocal:

1. 在多线程的类(如上述的Test类)中,创建一个ThreadLocal对象a.

2. 创建一个获取要隔离访问的数据的方法getXxx().(上述直接使用THreadlocal的get方法)。

3. 在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。(上述例子直接就使用THreadLocal的get方法)


四.ThreadLocal和同步机制的比较

       ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题

       在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

        而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线 程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编 写多线程代码时,可以把不安全的变量封装进ThreadLocal。

       当 然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了 同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的 有效方式而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线 程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所 以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间 的共享冲突,可以使用ThreadLocal,这将极大地简化你的程 序,使程序更加易读、简洁。

        概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。


五.参考

大多数内容都是以下3个网址的内容,然后对其中的一些例子加上自己的理解,有意见或者建议欢迎指出。

  1. http://lavasoft.blog.51cto.com/62575/51926/
  2. http://justsee.iteye.com/blog/791919
  3. http://blog.csdn.net/abing37/article/details/4460298