JAVA线程本地存储(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一个苹果出来,但这样做意义上就不太一样了,我看到办公室桌子上有个苹果,我要拿来用,结果却是我在自己口袋里掏出来一个。

看来要在苹果这个类上做文章了,你们每个人过来要苹果的人我都给你们一个苹果,然后你们就带在身上吧。

怎么带呢?好难描述啊,这里看下 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<Integer> 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<Apple> 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线程,自己写线程或者线程池的机会挺少的,都被类库或者框架给封装好了,我们要做的其实就是要理解概念和思路,以及别人是怎么给我们实现的

猜你喜欢

转载自blog.csdn.net/whut2010hj/article/details/81413887