黑马程序员--javaSE--ThreadLocal实现线程范围的共享变量

------- android培训java培训、期待与您交流! ----------


在写这篇博客之前,我登了下黑马论坛,想看一下自己提出的string和null的问题有回应没,
却意外地发现自己的技术分已经变成了24分,真是激动啊,这样我就不用在技术分上花太多的功夫了,
我也能更专注于写自己的技术博客了,真的很高兴啊!

这篇博客我要总结的是---ThreadLocal实现线程范围的共享变量,虽然自己感觉对他理解的不是很透彻,
但我还是打算写一下,也期望在写的过程中能够发现一些细节问题,巩固自己对这块内容的理解。

一、关于ThreadLocal
线程局部变量:
ThreadLocal是线程局部变量,其功能就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,
是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;
在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,
从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal的作用和目的:
用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
ThreadLocal的内部实现
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。
所以我们完全可以定义一个map集合区模拟ThreadLocal。
在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。

ThreadLocal的应用场景:
订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。

二、jdk中ThreadLocal的常用方法

void set(T value)  ------将此线程局部变量的当前线程副本中的值设置为指定值。
T get() --------  返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本

三、代码实现
这里我直接采用张老师(张孝祥)所提供的一个案例,并加以分析扩展
案例要求:
定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,
这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。

我们要做的就是实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。

这里先对基本类型的数据的封装举一个案例,体验一下ThreaLocal的具体应用
package com.itheima.thread;
import java.util.Random;
public class ThreadLocalTest {
//定义ThreadLocal类型的变量,其内部是采用map集合实现的,
//键是当前线程的名字,值是要存储到该线程的值,线程中所有的代码都能共享这个存储的值
static ThreadLocal<Integer> td = new ThreadLocal<Integer>();
public static void main(String[] args) {
	
	//采用循环的方式创建多个线程
	for(int i=0;i<3;i++){
		new Thread(new Runnable() {
			@Override
			public void run() {
				//产生一个随机数
				int data = new Random().nextInt(100);
				td.set(data);
				//对这个共享变量进行不同的代码操作
				new A().get();
				new B().get();
				
			}
		}).start();
	}
	
}

//将线程要执行的代码封装到类的方法中
static class A{
	public void get(){
		int data = td.get();
		System.out.println("A from " + Thread.currentThread().getName() 
				+ " get data :" + data);
	}
}

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

接着是对象类型的数据的封装,即让某个类针对不同线程分别创建一个独立的实例对象。

package com.itheima.thread;

import java.util.Random;

public class ThreadLocalData {
	public static void main(String[] args) {
		
		//采用循环的方式创建多个线程
		for(int i=0;i<3;i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					//产生一个随机数
					int data = new Random().nextInt(100);
					//获取共享数据包类的对象
					MyThreadScopeData threadInstance = MyThreadScopeData.getThreadInstance();
					//存储共享的数据
					threadInstance.setAge(data);
					threadInstance.setName("name"+data);
					//对这个共享变量进行不同的代码操作
					new A().get();
					new B().get();
					
				}
			}).start();
		}
		
	}
	
	//将线程要执行的代码封装到类的方法中
	static class A{
		public void get(){
			//因为是模拟的单例模式,所以此处获得是某个线程中的同一个共享数据包类的变量
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("A from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());
		
		}
	}
	
	static class B{
		public void get(){
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("B from " + Thread.currentThread().getName() 
					+ " getMyData: " + myData.getName() + "," +
					myData.getAge());
			}			
	}
}
	


/*
 * 实验步骤:
1.先在MyThreadLocalData类中定义一个访问权限为public的ThreadLocal类型的变量td,直接对这个td进行读写操作;
2.将变量td的访问权限定义为private, MyThreadLocalData上定义相应的set和get方法对向变量td中存储和检索数据;
3.将MyThreadLocalData类自身变成一个具有业务功能的对象,每个线程仅能有该类的一个实例对象,即对于不同的线程来说,
MyThreadLocalData.getMyData静态方法拿到的对象都不相同,但对于同一个线程来说,
不管调用MyThreadLocalData.getMyData多少次和在哪里调用,拿到的都是同一个MyThreadLocalData对象。
先将MyThreadLocalData封装成具有业务功能的对象,然后设计getMyData方法的定义,最后定义getMyData方法要操作的ThreadLocal变量和编写具体的代码
 */

//将所有的共享变量都封装到一个共享类中(此处封装的是String类型的name和int类型的age,也可以是其他的类的变量
class MyThreadScopeData{
	
	//上面的步骤3所述,所以要模拟单例的设计模式(这里采用饥饿式)
	public static MyThreadScopeData getThreadInstance(){
		//从线程中获取共享的数据类MyThreadScopeData
		MyThreadScopeData instance = td.get();
		if(instance == null){
			//为空说明某个线程的ThreadLocal类中没有共享的数据包(即MyThreadScopeData类的实例对象),就创建并添加
			instance = new MyThreadScopeData();
			td.set(instance);
		}
		return instance;
	}
	
	//定义能够存储线程共享变量的ThreadLocal类的实例,采用泛型的模式,泛型类型是所有共享变量打包的类MyThreadLocalData
	private static ThreadLocal<MyThreadScopeData> td = new ThreadLocal<MyThreadScopeData>();
	
	//在这说明数据包类所包含的数据
	private String name;
	private int age;
	public static ThreadLocal<MyThreadScopeData> getTd() {
		return td;
	}
	public static void setTd(ThreadLocal<MyThreadScopeData> td) {
		MyThreadScopeData.td = td;
	}
	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;
	}
	
}

上面的代码是张老师设计的,个人感觉设计的非常巧妙,采用模拟单例模式,在写这个封装了所有的对象的共享类时,
可以先将其设计为单例模式,再在该类中声明一个私有的静态的ThreadLocal类的对象,然后再将此对象以单例模式的
方式表现出来,对外接口的方法是public static ThreadLocal getThreadInstance()的方法。


此种方法设计的优点:将MyThreadLocalData类自身变成一个具有业务功能的对象,每个线程仅能有该类的一个实例对象,
即对于不同的线程来说,MyThreadLocalData。getThreadInstance静态方法拿到的对象都不相同,但对于同一个的线程来说,
不管调用MyThreadLocalData.getThreadInstance多少次和在哪里调用,拿到的都是一个具有MyThreadLocal对象。


四、总结:
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。 


当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。 


ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。 




ps:看了一天的java多线程,终于对其有些认识了,也稍微总结了下,算是完成了第四篇博客,但java多线程的东西还有很多,以后有机会一定还会
好好的在补充一下这方面的知识。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值