Java新技术---线程学习之ThreadLocal

 

ThreadLocal学习 

        我们知道,多线程共享数据时可能会存在并发错误,之前一直用synchronized这类线程同步的机制来解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容。在线程同步的机制中,多个线程不能同时访问共享数据,必须先后对变量的值进行访问或者修改,这是一种以延长访问时间来换取线程安全性的策略,必然降低了程序的运行效率。

       而ThreadLocal的出现为解决多线程并发问题提供了一种新思路。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。这就意味着竞争条件被彻底消除了,没有必要对这些线程进行同步,它们也就能最大限度的由CPU调度,并发执行。ThreadLocal访问的变量,都是自己独有的变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。与线程同步的机制对比,ThreadLocal是一种以空间来换取线程安全性的策略,运行效率较前者高。

ThreadLocal的接口方法

       ·void set(Object value)设置当前线程的线程局部变量的值;

       ·public Object get()该方法返回当前线程所对应的线程局部变量;

       ·protected Object initialValue()返回该线程局部变量的初始值,是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,缺省实现直接返回一个null。

       ·JDK 5.0新增了public void remove()方法删除当前线程局部变量的值,目的是为了减少内存的占用,并不是必须的操作,因为当线程结束后,对应该线程的局部变量将自动被垃圾回收,但显示调用该方法能够加快内存回收的速度。

       ·JDK 5.0出现泛型以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。方法分别是void set(T value)、T get()以及T initialValue()。

【例1】

import java.util.Random;

public class ThreadLocalTest {
	private static ThreadLocal<Integer> i = new ThreadLocal<Integer>();// 创建ThreadLocal

	public static void main(String[] args) {

		for (int x = 0; x < 2; x++) {// 创建2个线程
			new Thread(new Runnable() {

				@Override
				public void run() {
					int data = new Random().nextInt();// 获取随机数
					System.out.println(Thread.currentThread().getName()
							+ "has put a data" + data);
					i.set(data);// 将随机数放入当前线程的变量拷贝中
					new A().get();// 通过对象A获取当前线程的变量
					new B().get();// 通过对象B获取当前线程的变量
				}

			}).start();
		}
	}

	static class A {
		public void get() {
			int data = i.get();
			System.out.println("A from " + Thread.currentThread().getName()
					+ " get data :" + data);
		}
	}

	static class B {
		public void get() {
			int data = i.get();
			System.out.println("B from " + Thread.currentThread().getName()
					+ " get data :" + data);
		}
	}
}

运行结果:

Thread-1 has put a data1646705666
Thread-0 has put a data1387369058
A from Thread-1 get data :1646705666
A from Thread-0 get data :1387369058
B from Thread-0 get data :1387369058
B from Thread-1 get data :1646705666

       从运行结果可以看出:通过ThreadLocal创建的变量在多线程中是独立的,不管是通过模块A取出还是通过模式B取出的线程变量,只要线程相同,取出的变量就相同,线程不同,取出的变量也不相同,这正说明了每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

上面的程序中,每个线程的独立副本都是单个变量,如果我要给每个线程设置多个变量副本那怎么做?比如有2个变量副本,分别为整型和字符串类型。

分析:由于ThreadLocal支持泛型(泛型是引用类型),我们可以将这2个变量副本集中到集合或者类中,再设置或获取线程相关的类或集合,进而完成给每个线程存取多个独立变量的任务。

【每个线程独立操作多个变量】

import java.util.Random;

public class ThreadLocalTest {
	private static ThreadLocal<Integer> i = new ThreadLocal<Integer>();// 创建ThreadLocal

	private static ThreadLocal<ScopeData> scopeData = new ThreadLocal<ScopeData>();

	public static void main(String[] args) {

		for (int x = 0; x < 2; x++) {// 创建2个线程
			new Thread(new Runnable() {

				@Override
				public void run() {
					int data = new Random().nextInt();// 获取随机数
					System.out.println(Thread.currentThread().getName()
							+ " has put a data " + data);
					i.set(data);// 将随机数放入当前线程的变量拷贝中

					scopeData.set(new ScopeData());// 往当前线程放入ScopeData对象副本
					new A().get();// 通过对象A获取当前线程的变量
					new B().get();// 通过对象B获取当前线程的变量
				}

			}).start();
		}
	}

	static class A {
		public void get() {
			int data = i.get();// 取出当前线程的随机数
			ScopeData instance = scopeData.get();// 取出本线程的ScopeData对象
			instance.setAge(data);
			instance.setName("name" + data);
			System.out.println("A from " + Thread.currentThread().getName()
					+ " get age :" + instance.getAge() + " get name :"
					+ instance.getName());
		}
	}

	static class B {
		public void get() {
			int data = i.get();// 取出当前线程的随机数
			ScopeData instance = scopeData.get();// 取出本线程的ScopeData对象
			instance.setAge(data);
			instance.setName("name" + data);
			System.out.println("B from " + Thread.currentThread().getName()
					+ " get age :" + instance.getAge() + " get name :"
					+ instance.getName());
		}
	}

}

class ScopeData {
	private String name;
	int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

运行结果:

Thread-1 has put a data 1095498667
Thread-0 has put a data -1482596895
A from Thread-0 get age :-1482596895 get name :name-1482596895
A from Thread-1 get age :1095498667 get name :name1095498667
B from Thread-0 get age :-1482596895 get name :name-1482596895
B from Thread-1 get age :1095498667 get name :name1095498667

       上面的运行结果证明,ThreadLocal存取的对象是独立的,通过多个模块(A和B)获取的数据都是各自线程的数据,线程之间数据操作不会相互影响。

通过上面2个例子可以看出,ThreadLocal实现的多进程并发不需要同步机制也能消除并发错误,因为它们操作的都是各自的副本,就像线程是隔离的,那它是如何做到的呢?

查看存取数据的set()和get()方法源码:

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

       从上面源码可以知道,ThreadLocal线程隔离的关键在于ThreadLocal类的一个静态内部类ThreadLocalMap,它实现了键值对的设置和获取(ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。

       set()和get()这两个方法的代码可知,在get()当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这和set()方法的代码对应。

       至此我们知道了ThreadLocal在多线程环境中不需要同步机制也不会出现并发问题,因为它使得每个线程操作的变量副本都是独立的。回顾之前的单例设计,我们知道懒汉式是存在多线程并发隐患的,需要同步机制解决,现在我们有了ThreadLocal就可以优雅的实现懒汉式单例设计。

【懒汉式单例设计】

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class ThreadLocalTest {

	private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
	private static ThreadLocal<MyThreadScopeData> myThreadScopeData = new ThreadLocal<MyThreadScopeData>();

	public static void main(String[] args) {
		for (int i = 0; i < 2; i++) {// 创建2个线程
			new Thread(new Runnable() {
				@Override
				public void run() {
					int data = new Random().nextInt();// 获取一个随机数
					System.out.println(Thread.currentThread().getName()
							+ " has put data :" + data);
					x.set(data);// 将一个随机数放进ThreadLocal<Integer>对象中
					MyThreadScopeData.getThreadInstance()// 获取MyThreadScopeData对象
							.setName("name" + data);// 设置该对象的姓名为"name" + data
													// 字符串
					MyThreadScopeData.getThreadInstance().setAge(data);// 获取实例并设置年龄为data
					new A().get();// 通过模块A获取x和myThreadScopeData的数据
					new B().get();// 通过模块B获取x和myThreadScopeData的数据
				}
			}).start();
		}
	}

	static class A {
		public void get() {
			int data = x.get();// 获取当前线程存储的随机数
			System.out.println("A from " + Thread.currentThread().getName()
					+ " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out
					.println("A from " + Thread.currentThread().getName()
							+ " getMyData: " + myData.getName() + ","
							+ myData.getAge());// 输出当前线程MyThreadScopeData实例的姓名年龄
		}
	}

	static class B {
		public void get() {
			int data = x.get();// 获取当前线程存储的随机数
			System.out.println("B from " + Thread.currentThread().getName()
					+ " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out
					.println("B from " + Thread.currentThread().getName()
							+ " getMyData: " + myData.getName() + ","
							+ myData.getAge());// 输出当前线程MyThreadScopeData实例的姓名年龄
		}
	}
}

// 单例设计
class MyThreadScopeData {
	private MyThreadScopeData() {
	}

	public static MyThreadScopeData getThreadInstance() {
		MyThreadScopeData instance = map.get();
		if (instance == null) {
			instance = new MyThreadScopeData();
			map.set(instance);// 如果map没有实例,就创建一个实例放进map中
		}
		return instance;// 如果map.get()有实例就返回该实例
	}

	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();

	private String name;
	private int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

运行结果:

Thread-0 has put data :-477718897
Thread-1 has put data :972697909
A from Thread-1 get data :972697909
A from Thread-1 getMyData: name972697909,972697909
A from Thread-0 get data :-477718897
A from Thread-0 getMyData: name-477718897,-477718897
B from Thread-1 get data :972697909
B from Thread-0 get data :-477718897
B from Thread-1 getMyData: name972697909,972697909
B from Thread-0 getMyData: name-477718897,-477718897

从运行结果可见,不需要同步也能解决并发隐患,所以比同步机制效率高,但占用内存会比同步机制大,是一种空间换时间的方案。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值