对ThreadLocal的理解
ThreadLocal类就相当于一个Map,用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
怎样得到线程结束的通知呢,或者是监听线程死亡的事件?比如监听虚拟机退出,Runtime类代表虚拟机,其addShutdownHook(Thread hook)方法会在虚拟机停止前运行传入线程的代码。那么要得到线程结束的通知,也会用到同样的思想。
ThreadLocal的应用场景:
1.订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
2. 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
3.例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
通过一个例子加深对ThreadLocal应用场景的理解:
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close();
}
}
假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。
这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。
到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if(connect!=null)
connect.close();
}
}
class Dao{
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
//使用connection进行操作
connectionManager.closeConnection();
}
}
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。
那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
hreadLocal类提供的几个方法:
public
T get() { }
public
void
set(T value) { }
public
void
remove() { }
protected
T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法.
实验案例:定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。
实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
总结:一个ThreadLocal代表一个变量,故其中里只能放一个数据,你有两个变量都要线程范围内共享,则要定义两个ThreadLocal对象。如果有一个百个变量要线程共享呢?那请先定义一个实体对象来装这一百个变量,然后在ThreadLocal中存储这一个对象。比如定义一个学生实体,存放姓名,年龄等变量。
如何设计线程范围内的共享对象?
第一种不优雅的实现方式:
package cn.itcast.heima;
import java.util.Random;
public class ThreadLocalTest {
//定义一个ThreadLocal变量存储线程内共享变量
private static ThreadLocal x = new ThreadLocal();
//定义一个ThreadLocal变量存储线程内的实体对象
private static ThreadLocal myThreadScopeData = new ThreadLocal();
public static void main(String[] args) {
for(int i=0;i<2;i++){
new Thread(new Runnable(){
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName()+" has put data: "+data);
x.set(data);//存入数据,并且数据已与当前线程关联
//创建实体对象,并向实体对象中存入数据
MyThreadScopeData myData = new MyThreadScopeData();
myData.setName("name"+data);
myData.setAge(data);
//将实体存入ThreadLocal变量中
myThreadScopeData.set(myData);
new A().get();
new B().get();
}
}).start();
}
}
//模块A
static class A{
public void get(){
int data = x.get();//不用指定线程号就取出当前线程中变量的值
System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
//获取线程内存入的实体对象并获取对象中存储的数据
MyThreadScopeData myData = myThreadScopeData.get();
System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
}
}
//模块B
static class B{
public void get(){
int data = x.get();
System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
}
}
}
//定义一个实体
class 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;
}
}
第二种优雅的实现方式:
package cn.itcast.heima;
import java.util.Random;
public class ThreadLocalTest {
//定义一个ThreadLocal变量存储线程内共享变量
private static ThreadLocal x = new ThreadLocal();
public static void main(String[] args) {
for(int i=0;i<2;i++){
new Thread(new Runnable(){
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName()+" has put data: "+data);
x.set(data);//存入数据,并且数据已与当前线程关联
/*
//创建实体对象,并向实体对象中存入数据
MyThreadScopeData myData = new MyThreadScopeData();
myData.setName("name"+data);
myData.setAge(data);
//将实体存入ThreadLocal变量中
myThreadScopeData.set(myData);*/
//拿到与本线程相关的实例对象,然后向实体对象中存入数据
MyThreadScopeData.getThreadInstance().setName("name"+data);;
MyThreadScopeData.getThreadInstance().setAge(data);;
new A().get();
new B().get();
}
}).start();
}
}
//模块A
static class A{
public void get(){
int data = x.get();//不用指定线程号就取出当前线程中变量的值
System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
/*//获取线程内存入的实体对象并获取对象中存储的数据
MyThreadScopeData myData = myThreadScopeData.get();
System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
*/
//获取本线程相关的实体对象并获取对象中存储的数据
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
}
}
//模块B
static class B{
public void get(){
int data = x.get();
System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("B from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
}
}
}
//定义一个实体
class MyThreadScopeData{
//将该类设计成类似单例模式
private MyThreadScopeData(){}
private static ThreadLocal map = new ThreadLocal();
public static MyThreadScopeData getThreadInstance(){//这里不需要同步,因为ThreadLocal变量与当前线程相关,各个线程会获取各自的实例
MyThreadScopeData instance = map.get();
if(instance==null){//如果实例没有,就创建实例并保存
instance = new MyThreadScopeData();//保证返回的变量不为null
map.set(instance);//存入的实体与当前线程相关,所以该方法不需要加同步
}
return instance;
}
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;
}
}
优点:
与前一种实现方式相比,这种方式向调用者隐藏ThreadLocal变量
调用者只需要调用实体类中的方法就可以获得与当前线程相关的实例对象了
单例是在任意地方都只能获取同一个对象,这里是在线程内调用获取的是同一个对象