秋招准备-Java-并发编程-ThreadLocal(六)



1.结构分析

2.源码分析

3.使用和实验


1.ThreadLocal结构分析

    1.ThreadLocal<T>

    ThreadLocal提供了局部变量,主要是通过这个类,将一些线程需要的,本来可能是各个线程共享的变量(Connection),或者是约定好的初始变量(Integer i=0),变成线程自己的私有变量。

    即本来许多线程共享一个数据库连接Connection对象,这种情况,如果开同步会造成大量线程阻塞,如果不开则会有一个线程刚开启Connection另一个线程就关闭的现象,于是,用ThreadLocal,每个线程拥有一个Connection对象,自己开自己的关自己的。这就是ThreadLocal的用法。

    那么主要问题是,怎么通过ThreadLocal给每个线程分配线程本地变量,即是通过重写ThreadLocal的方法。

class ThreadLocal<T>
{
    protected T initialValue() //(3)
    {	return null;	}
    //注释部分可以先不看
    private T setInitialValue() 
    {
        T value = initialValue();//(2)
        /*Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);*/
        return value;
    }
    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();//(1)
    }
}

    对于我们想要设置成为线程本地变量的这个T类型的变量,我们的获得方法,就是get(),当调用get()后,看代码,前两行可以理解成一个寻找操作,然后,如果没找到,就到(1),返回设置初始化的值。

    然后看到这个方法,到(2),即将初始化的那个返回拿过来,然后进行了一个设置工作,这部分展示不管,反正最后返回的就是initialValue()的返回值。

    然后到(3),这里默认返回null,那么我们只要重写这个方法,然后返回我们想要的初始值,那么就可以在get()这得到了。

    比如:

Class Test extends Thread
{
	private ThreadLocal local = new ThreadLocal<Connection>
	{
		protected Connection initialValue()
	    {	
			Class.forName("com.mysql.jdbc.Driver");
			return Driver.getConnection("jdbc:mysql://localhost:3306/db_name","user","psd");
	    }
	}
	public void run()
	{
		Connection con = local.get();
	}
}

    这个ThreadLocal对象可以写在别的类里,然后定义成public static,在每个线程中调用get(),然后会带着需要的泛型变量T,存储到不同线程里,要了解它的存储形式,需要继续了解一些结构。

    

    2.ThreadLocalMap&&Entry<key,value>

    这是一个写在ThreadLocal里的内部类,显然它是一个Map的实现类,然后它的节点类Entry<key,value>。

    显然,底层的存储是通过ThreadLocalMap来实现的,真正的线程本地变量是存储在ThreadLocalMap实例对象中的,并且是在这个Map对象的某个节点Entry上的。

    而Entry中,key是ThreadLocal变量,value才是线程本地变量。因此是一个ThreadLocal对象对应一个value变量。


    3.Thread&&threadLocal变量

    实际上,每一个线程都有一个ThreadLocalMap实例对象,也就是在Thread类里就有这么一个成员变量threadLocals。

    刚才说了,ThreadLocal一般会写在某个类(A)里,写成public static,那么它实际上就是一个静态类对象了,在这个ThreadLocal类中根据需求重写好initialValue()方法后,就可以通过get()来得到初始值。

    那么现在我有了一个ThreadLocal对象,A.local,与起初始化值con(通过get()获得),当在任意线程(B)调用A.local.get()时,现在可以来看我刚才注释掉的内容。

    用文字来形容,则是,在get()方法里,先获得当前线程(B),然后获得B中的threadLocals,在使用这个ThreadLocalMap变量时,肯定需要做判空和初始化这个Map的操作,当它完成后,我们就把Entry<A.local,con>这个节点,存进了这个线程的threadLocals这个Map里,以后再用的时候,因为每次用的Map都是这个线程自己的Map,所以通过A.local,就能唯一的找到con。具体实现和一些方法,自然是写在源码里的。



2.源码分析

    了解完结构,再来简单分析一下ThreadLocal的具体实现,只说在ThreadLocal上的代码。

class ThreadLocal
{
    public ThreadLocal();
    protected T initialValue();
    private T setInitialValue();
    public T get() ;
    public void set(T value);
    public void remove() ;
    ThreadLocalMap getMap(Thread t);
    void createMap(Thread t, T firstValue);
}
    1.先看get()方法
    public T get() {
        Thread t = Thread.currentThread();//(1)
        ThreadLocalMap map = getMap(t);//(2)
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//(3)
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    (1)获得当前线程,(2)获得当前线程的ThreadLocalMap对象threadLocals

    接下来,如果map不为空,那就在这个map里找<ThreadLocal,value>键值对,(3)则是ThreadLocalMap里的方法,用来通过ThreadLocal对象找相应节点Entry。因此是this,指当前的ThreadLocal对象。

    如过找到了,e不为空,那么就是有这个键值对,就可以得到value值,做下转换就返回result。

    如果找不到节点,或者map都没被初始化,(Thread里threadLocals初始为null),那么进入setInitialValue()方法。

    然后,最终都会返回这个ThreadLocal设置的对应线程本地变量。


    2.setInitialValue()
    private T setInitialValue() {
        T value = initialValue();//(1)
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//(2)
        else
            createMap(t, value);//(3)
        return value;
    }

    当需要用到setInitialValue()方法时,即线程的map没初始化或者map里没相应节点,这时候都需要新建节点,因此需要初始值,通过(1)获得。

    接下来,又是获得线程与线程的map,因此每个操作其实都是操纵的当前线程与它的map。

    map非空时,只需要将<ThreadLocal,value>键值对存进去,因此传入参数是this和value,即(2),显然ThreadLocalMap有相应的新建节点和加到Map里的方法。

    map为空时,需要初始化当前线程的threadLocals变量并且添加第一个节点,即(3)

    createMap(Thread,T)是ThreadLocal的方法,下面分析,作用即是初始化传入Thread线程的threadLocals,并添加第一个节点。


    3.initialValue()
    protected T initialValue() {
        return null;
    }

    现在再来看initialValue(),显然,具体实现上,就只需要重写这个方法了。

    如果要实现一个返回自增不重复ID的ThreadLocal,那么就是这样写:

public static ThreadLocal local = new ThreadLocal<Integer>(){
	private AtomicInteger i = new AtomicInteger(1);
	protected Integer initialValue()
	{
		return i.getAndIncrement();
	}
};

    类似数据库连接那样的,那就是这样的:

public static ThreadLocal local = new ThreadLocal<Connection>{
	protected Connection initialValue()
    {	
		Class.forName("com.mysql.jdbc.Driver");
		return Driver.getConnection("jdbc:mysql://localhost:3306/db_name","user","psd");
    }
};


    4.getMap(Thread t)
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    获得当前线程的ThreadLocalMap是写在ThreadLocal里的,显然这个已经很好理解了。

    Thread.currentThread()获得当前线程,然后返回它的threadLocals就行,其实直接用也行,这里只是加了个方法的包装。

    Thread,TheadLocal同处于java.lang包下,Thread中threadLocals无访问限制符,即default,因此同包可见。


    5.createMap(Thread t, T firstValue)
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    显然,这个方法用于创建一个含有<this,value>节点的ThreadLocalMap,然后让t.threadLocals指向它。


    6.set(T value)
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    每一个方法基本都有获得线程t和它的map,然后这个set的作用相当于setInitialValue()+initialValue(),当调用get()时,是map中找不到当前ThreadLocal对象的key值和map没有初始化时,才用setInitialValue()+initialValue()初始化,因此当完成这一步时,map一定初始化好,节点也一定存在。

    而set也是相同的作用,只是它是主动调用的,同样确保,如果map不存在,则初始化map并添加节点。如果map存在,则添加键值对,而如果用set()的话,就会存在value值覆盖的问题。

    如果在map中,以当前ThreadLocal对象为key的键值对已存在,那么set()后,会用set(T value)参数中的value来更新这个键值对中的value值。


    7.remove()
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

    获得当前线程的map,如果非空,则删除以当前ThreadLocal对象为key值的节点。

    ThreadLocalMap中的remove()操纵中涉及到Map的遍历,节点的删除。

    remove()之后,确保了当前线程的map中无以this为key的节点。



3.使用和实验

    其实说明了initialValue()的重写方法后,就是靠这个来使用ThreadLocal了。

    那么先写一段用ThreadLocal来实现为多线程添加递增不重复ID的代码:

class A
{
	public ThreadLocal<Integer> local = new ThreadLocal<Integer>()
		{
			private AtomicInteger i = new AtomicInteger(1);
			protected Integer initialValue()
			{
				return i.getAndIncrement();
			}
		};
}
public class Main
{
	public static void main(String[] args)throws Exception 
	{
		A a = new A();
		A b = new A();
		for(int i=0;i<3;i++)
		{
			Thread t = new Thread(new Runnable() {
				public void run()
				{
					Integer I = a.local.get();
					System.out.println(I);
				}
			});
			t.start();
			t.join();
		}
		for(int i=0;i<3;i++)
		{
			Thread t = new Thread(new Runnable() {
				public void run()
				{
					Integer I = a.local.get();//1
//					Integer I = b.local.get();//2
					System.out.println(I);
				}
			});
			t.start();
			t.join();
		}
	}
}
//使用注释行1,则六个线程都是存的a.local,其原子变量也增到了6,因此输出1~6
//使用注释行2,则前三个存a.local,后三个存b.local,输出1~3,1~3

    并且,线程的threadLocals是Map,因此是能存多个<ThreadLocal,value>键值对的。

    总结:

    ThreadLocal和同步机制对线程来说是两种处理数据的方法,当多线程之间需要共享数据的情况下,就需要用同步机制来保证线程安全。

    而当多线程直接没有共享数据的情况下,又需要操纵一些公有的数据,因此可以自定义一个ThreadLocal的实现类,按照自己的需求和数据的类型,来重写initialValue(),然后存储到线程自己的threadLocals这个Map中,因此会被叫做线程本地变量。

    多用于解决数据库连接、Session管理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值