今天看书看到这么一句话,“防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储”。(第一种方式就是进行同步控制,比如加锁喽)
那么什么是线程本地存储,个人理解就是,对一个苹果,大家都把苹果放在一个桌子上存放(给域赋值),等你去拿的时候发现拿到的可能不是自己那个苹果了,ThreadLocal的做法就是你的苹果,你放在那,等你去拿,拿到的就是你原先那个。
假设我们现在有个共享的苹果存放区,人人都想把苹果放在上面(把苹果的ID改为自己的ID),在不做同步控制的情况下,很快就会发现明明刻上自己名字的苹果,拿到手里一看居然是别人的名字,哈哈,我们试验一下。
写个Apple类,就一个实例域AppleID,一个 set 一个 get
package dailyprg0804;
public class Apple {
private int AppleID;
Apple(){}
Apple(int AppleID){
this.AppleID = AppleID;
}
public int getAppleID() {
return AppleID;
}
public void setAppleID (int appleID) {
AppleID = appleID;
}
}
写一个测试类,除main外跑5个线程,用线程池
package dailyprg0804;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalTest {
public static void main(String[] args) throws Exception{
Apple apple = new Apple();
class RunApple implements Runnable{
private final int id;
RunApple(int id){
this.id = id;
}
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
apple.setAppleID(id);
Thread.yield();
System.out.println(this);
}
}
@Override
public String toString(){
return "#" + id + ":" + apple.getAppleID();
}
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
exec.execute(new RunApple(i));
}
TimeUnit.SECONDS.sleep(1);
exec.shutdownNow();
}
}
稍微运行一会儿就关掉线程池,看下输出
可以发现已经分不清是谁的苹果了。
为了解决这个问题,直接点就是加锁,synchronized或者Lock,我们就用synchronized试下,我们把“给苹果刻上自己名字然后看苹果上是谁的名字”这个过程同步起来,哈哈,就是一个人把苹果放上去,然后霸占着桌子,不让别人放。
稍微修改下代码
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
synchronized(apple){
apple.setAppleID(id);
Thread.yield();
System.out.println(this);
}
}
稍微运行久一点,看下结果
没有问题的,毕竟加锁了
说回 ThreadLocal,怎么确保拿到的是自己的苹果呢。
当然我们可以在任务里面new一个苹果出来,但这样做意义上就不太一样了,我的苹果一直放在我口袋里,没放到桌子上,就是局部变量,局部变量的缺点就是我知道我有个苹果,但我同事不知道,局部变量不能被线程里所有的方法可见,可能会造成额外的参数传递,或者多次new苹果出来。。
看来要在苹果这个类上做文章了。
怎么带呢?好难描述啊,这里看下 Thread 和 ThreadLocal 类的源码吧。
在Thread类中有个ThreadLocalMap
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这个map类的键值对是
Entry(ThreadLocal> k, Object v)
就是这个map在每个Thread里存了ThreadLocal对象和对应的其他某个对象,这里的某个对象就可以是我们说的苹果。
ThreadLocal类有两个重要方法 set 和 get ,
get就是获取这个ThreadLocal变量在当前类的值,源码如下
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
可以看到这个方法先要获取当前线程的map,然后在 map 里面找 this ThreadLocal 变量对应的值。
如果这个对象没有值就调用setInitialValue()
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
protected T initialValue() {
return null;
}
再看set,set就是给当前线程设置一个ThreadLocal变量对应的值,这个值是Object
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
GET 和 SET 都有这个一句
Thread t = Thread.currentThread();
就是定位到了当前线程。
到此,我们可以重写一下Apple类并做下测试了。
package dailyprg0804;
public class Apple {
private final ThreadLocal appleThreadLocal
= new ThreadLocal<>();
public int getAppleID() {
return appleThreadLocal.get();
}
public void setAppleID(int appleID) {
appleThreadLocal.set(appleID);
}
}
就是我们在设置AppleID的时候其实是在设置Thread本地ThreadLocalMap里的appleThreadLocal对应的ID,
这样修改了Apple,测试类的代码就不用动了,还是那个没有synchronized的版本,
我们测试下
每个线程都是在修改自己本地的变量,相互之间没有交集,所以不会有冲突。
但是这样写放到线程本地的只是苹果的AppleID了,也就是一个整数,而不是苹果这个对象,我们再修改一下代码,不修改Apple这个类,把ThreadLocal的实例放到测试类里面去,
Apple类的代码不变,
package dailyprg0804;
public class Apple {
private int AppleID;
Apple(){}
Apple(int AppleID){
this.AppleID = AppleID;
}
public int getAppleID() {
return AppleID;
}
public void setAppleID (int appleID) {
AppleID = appleID;
}
}
修改测试类
package dailyprg0804;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalTest {
private static ThreadLocal appleThreadLocal
= new ThreadLocal<>();
public static void main(String[] args) throws Exception{
class RunApple implements Runnable{
private final int id;
RunApple(int id){
this.id = id;
}
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
appleThreadLocal.set(new Apple(id));
Thread.yield();
System.out.println(this);
}
}
@Override
public String toString(){
return "#" + id + ":" + appleThreadLocal.get().getAppleID();
}
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
exec.execute(new RunApple(i));
}
TimeUnit.SECONDS.sleep(1);
exec.shutdownNow();
}
}
测试一下
也是OK的。
另外还在其他书上看到过,如果线程自然死亡,那么ThreadLcoal的数据也会跟着消失,如果是线程池,在线程会被复用的情况下,要手动清除。可以把map中的值set成null,让GC可以工作,也可以调用ThreadLocal的remove方法。
并发编程实战这本书里也提到ThreadLocal会降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
有位博主说过,对于Java线程,自己写线程或者线程池的机会挺少的,都被类库或者框架给封装好了,我们要做的其实就是要理解概念和思路,以及别人是怎么给我们实现的。