JAVA线程本地存储(ThreadLocal)

今天看书看到这么一句话,“防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储”。(第一种方式就是进行同步控制,比如加锁喽)

那么什么是线程本地存储,个人理解就是,对一个苹果,大家都把苹果放在一个桌子上存放(给域赋值),等你去拿的时候发现拿到的可能不是自己那个苹果了,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();
    }
}

稍微运行一会儿就关掉线程池,看下输出

JAVA线程本地存储(ThreadLocal)_第1张图片

可以发现已经分不清是谁的苹果了。

为了解决这个问题,直接点就是加锁,synchronized或者Lock,我们就用synchronized试下,我们把“给苹果刻上自己名字然后看苹果上是谁的名字”这个过程同步起来,哈哈,就是一个人把苹果放上去,然后霸占着桌子,不让别人放。

稍微修改下代码

            @Override
            public void run(){
                while(!Thread.currentThread().isInterrupted()){
                    synchronized(apple){
                        apple.setAppleID(id);
                        Thread.yield();
                        System.out.println(this);
                    }
                }

稍微运行久一点,看下结果

JAVA线程本地存储(ThreadLocal)_第2张图片

没有问题的,毕竟加锁了

说回 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的版本,

我们测试下

JAVA线程本地存储(ThreadLocal)_第3张图片

每个线程都是在修改自己本地的变量,相互之间没有交集,所以不会有冲突。

但是这样写放到线程本地的只是苹果的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();
    }
}

测试一下

JAVA线程本地存储(ThreadLocal)_第4张图片

也是OK的。

另外还在其他书上看到过,如果线程自然死亡,那么ThreadLcoal的数据也会跟着消失,如果是线程池,在线程会被复用的情况下,要手动清除。可以把map中的值set成null,让GC可以工作,也可以调用ThreadLocal的remove方法。

并发编程实战这本书里也提到ThreadLocal会降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

有位博主说过,对于Java线程,自己写线程或者线程池的机会挺少的,都被类库或者框架给封装好了,我们要做的其实就是要理解概念和思路,以及别人是怎么给我们实现的

 

 

你可能感兴趣的:(JAVA学习,ThreadLocal,并发)