您的位置:首页 > 编程语言 > Java开发

Java多线程/并发15、保持线程间的数据独立:ThreadLocal应用

2017-04-28 17:03 330 查看
文档上(http://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html)这么写的

This class provides thread-local variables. These variables differ

from their normal counterparts in that each thread that accesses one

(via its get or set method) has its own, independently initialized

copy of the variable. ThreadLocal instances are typically private

static fields in classes that wish to associate state with a thread

(e.g., a user ID or Transaction ID).

这段说了三个事:

1、使用ThreadLocal定义的变量,其实在每个线程创造了一个副本。假设在类里定义了一个
ThreadLocal<Integer> n
变量。那么如果有5个线程访问n,就会为每个线程产生一个n的副本,总共5个。如果有100个线程访问,那就有100个副本。可以想成java帮你创建了一个
Map<Thread, T>
容器。

2、
ThreadLocal<Integer> n
变量可以赋初使值的。通过实现ThreadLocal的initialValue()方法,返回一个值。这样每个线程在访问时,获得的副本都用这个值初始化了。

3、在一个线程安全的类里,常用的做法是用
ThreadLocal<T>
private static
类型的字段建立一个绑定关系。这一句是对应用场景非常重要的一个描述。

现在光看文字比较晦涩。下面结合代码理解。

想像一个场景,我们构建了一个积分商城,很多用户在平台上做任务和兑换奖品,这样会产生积分的增减。

我们构造一个Customer类来完成这个功能。

class Customer {
private int userid;
private int account;

public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
/* 初使化用户积分余额 */
public Customer(int userid, int account) {
this.userid = userid;
this.account = account;
}
/* 增加操作。同步方法,避免其它线程在同时操作相同用户的帐户 */
public synchronized void increase(int m) {
this.account += m;
System.out.println("用户[id:" + String.valueOf(userid) + "]存入"
+ String.valueOf(m) + "点,积分余额" + account + "点");
}
/* 减少操作。同步方法,避免其它线程在同时操作相同用户的帐户 */
public synchronized void decrease(int m) {
if ((account - m) >= 0) {
this.account -= m;
System.out.println("用户[id:" + String.valueOf(userid) + "]消费"
+ String.valueOf(m) + "点,积分余额" + account + "点");
} else {
System.err.println("用户[id:" + String.valueOf(userid) + "]积分不足,当前积分"
+ String.valueOf(account) + "点,期望兑换商品需" + String.valueOf(m)+ "点") ;
}
}
}


现在我们用两个线程模拟用户在平台上“增加”和“兑换”两个动作导致的积分变化。

两个用户之间是互相独立的。

正常做法在每个线程中new一个Customer对像实例即可,不同线程中的变量是互不干扰的

public class ThreadLocalTest {

//static Customer someone=new Customer(0,3000);

public static void main(String[] args) {
for (int m = 0; m < 2; m++) {
new Thread() {
public void run() {
/* 初始化用户,设置初始账户积分 1千 */
Customer someone = new Customer(new Random().nextInt(1000),
3000);
/* 模拟操作增加2次积分 */
for (int i = 0; i < 2; i++) {
someone.increase(new Random().nextInt(500));
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/* 模拟兑换商品消耗积分2次 */
for (int i = 0; i < 2; i++) {
someone.decrease(new Random().nextInt(3700));
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"线程中用户[id:"+someone.getUserid()+"] 余额: "
+ someone.getAccount());
}
}.start();
}
}
}


运行结果:

用户[id:957]存入304点,积分余额3304点
用户[id:640]存入214点,积分余额3214点
用户[id:640]存入284点,积分余额3498点
用户[id:957]存入60点,积分余额3364点
用户[id:957]消费1357点,积分余额2007点
用户[id:640]消费2424点,积分余额1074点
用户[id:640]消费547点,积分余额527点
用户[id:957]积分不足,当前积分2007点,期望兑换商品需3326点
Thread-1线程中用户[id:957] 余额: 2007
Thread-0线程中用户[id:640] 余额: 527


现在程序运行地非常好,压根不需要ThreadLocal出现。因为要保持线程间的数据独立,那么就在线程中定义局部变量,这样变量之间都是独立的,互相不会产生影响。

但是问题还是来了。

现代程序开发中,会用到容器统一管理对象,用统一的接口实现对象注入。假设Customer对象是由某个方法统一生成,并且要求统一初始配置一些字段,要怎么办呢?这时,程序会发生一些改变,让我们来模拟一下:

1、首先新增一个外部容器类,获取对象为singleton(为了简化没有使用hashmap,该容器里就一个对象customer)

/*
* 定义一个容器,该容器有一个方法获取Customer类实例
* 假设容器获取的对像是单例模式
* 初始化Customer类帐户,设置初始积分为1000
*/
class Container {
private static Customer customer;
public static Customer getCustomer() {
if(customer==null){
customer=new Customer(0, 1000);//硬编码
}
return customer;
}
}


2、修改线程中对像产生方式:

将以前的

Customer someone = new Customer(new Random().nextInt(1000),
3000);


替换成

Customer someone = Container.getCustomer();
someone.setUserid(new Random().nextInt(1000));


重新运行程序:

用户[id:624]存入146点,积分余额3146点
用户[id:824]存入272点,积分余额3418点
用户[id:824]存入494点,积分余额3912点
用户[id:824]存入152点,积分余额4064点
用户[id:824]消费3087点,积分余额977点
用户[id:824]积分不足,当前积分977点,期望兑换商品需3571点
用户[id:824]积分不足,当前积分977点,期望兑换商品需2104点
用户[id:824]消费678点,积分余额299点
Thread-0线程中用户[id:824] 余额: 299
Thread-1线程中用户[id:824] 余额: 299


出问题了。可以看到id:624的用户被替换了,两个线程中都只有id:824的用户,且最终余额一样。说明线程间数据没有独立,相互影响了。后运行的线程产生的Customer对象,覆盖了前面运行的线程中产生的对象。问题在于Container容器中的单例对象customer是静态变量,静态变量是存储在方法区,所有线程共享的。

说了半天,现在该轮到ThreadLocal上场了。

即然是容器类Container提供的对象造成了两个线程间不能独立(Container类非线程安全的),那我们就想办法修改Container容器,让它提供的对象在不同线程间都是独立的,换句话说,每个线程都有一份独立的Customer副本,互不影响。

改造后的如下:

class Container {
private static Customer customer;
private static ThreadLocal<Customer> Local_Customer = new ThreadLocal<Customer>(){
@Override
protected Customer initialValue() {
/*由于customer当前为null,所以实际每个线程获得的是一个null对象 */
return customer;
}
};
public static Customer getCustomer() {
if(Local_Customer.get()==null){
Local_Customer.set(new Customer(0, 3000));//硬编码
}
return Local_Customer.get();
}
}


运行,可以看到一切正常了。

ThreadLocal提供了get(),set()方法,让线程设置和获取变量,简单方便。这里要提到的是initialValue重载方法。initialValue方法返回一个实例,这个实例干嘛用的呢?用来初始化线程获取的副本的值。我们想像ThreadLocal是一个
map<thread,value>
的容器,如果不重载initialValue()方法,那么thread-1开始访问时,容器是这样的

map<"thread-1",null>
,thread-2再访问,容器会变成

map<"thread-1",null>


map<"thread-2",null>


如果重载initialValue()方法并返回值的话,

thread-1开始访问时,容器是这样的

map<"thread-1",return_object_value>


thread-2再访问,容器会变成

map<"thread-1",return_object_value>


map<"thread-2",return_object_value>


当然return_object_value也有可能是null,在上面这个例子中,return的值其实是null。

现在来看一个坑:

在最前面new一个对象,然后在initialValue()中返回该对象,这样每个线程不都初始化了一个3000积分的对象吗?

class Container {
/*提前初始化好对象,对象有3000积分*/
private static Customer customer = new Customer(0, 3000);
private static ThreadLocal<Customer> Local_Customer = new ThreadLocal<Customer>(){
@Override
protected Customer initialValue() {
return customer;
}
};
public static Customer getCustomer() {
/*这段都不需要了,因为一开始就new了对象,当线程获取副本时就不可能为空了*/
/*if(Local_Customer.get()==null){
Local_Customer.set(new Customer(0, 3000));//硬编码
}*/
return Local_Customer.get();
}
}


看起来挺有道理的,不过这里隐藏着一个大坑,运行一下你就会发现,又出问题了。

问题就出在
return Local_Customer.get()


之前定义了customer对象并用new 实例化了。而java中对于复杂对象,通常都是指针传递,因此这里的return Local_Customer.get() 返回的是指向coutomer的一个指针(或者叫引用)。这样导致所有线程获得的是一个指针的拷贝,所有线程中的指针指向的是同一块地址,即costomer对象。所有的线程都在操作同一个costomer对象,不出问题才怪。

明白了后,上面可以改造成:

class Container {
private static ThreadLocal<Customer> Local_Customer = new ThreadLocal<Customer>(){
@Override
protected Customer initialValue() {
/*初始化3000积分*/
return new Customer(0, 3000);
}
};
public static Customer getCustomer() {
return Local_Customer.get();
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: