ThreadLocal用法与原理以及在Spring事务管理中的应用

目录

一、初识ThreadLocal - 线程局部变量

二、用法举例 -  上手很简单  

三、原理分析 - 一探究竟

3.1 ThreadLocal.set() 方法

3.2 ThreadLocacl.get() 方法

四、运用场景 - 事务管理

4.1 构建场景

4.2 开启事务管理

4.2.1 反例一

4.2.2 反例二

4.2.3 正解


一、初识ThreadLocal - 线程局部变量

    ThreadLocal 是一个类名,但从字面意思理解,ThreadLocal也可以称为「线程局部变量」,也就是说,在某个线程运行的过程中,它往ThreadLocal这里类里存储的值或对象只属于当前这个线程,别的线程无法访问。

    假如我们现在有一个ThreadLocal对象,多个线程[A, B, C]同时运行, 它们都通过这个ThreadLocal对象存储了一个值,那么它们中任何一个线程在任何时刻取回的值一定就是当初自己设置的那个值,不会与其他线程发生错乱,也相当于数据和线程是绑定在一起了,起到了线程之间数据隔离的作用。

    更加通俗一点的解释:你可以先将ThreadLocal当做一个公共仓库,任何线程都可以往里面存放东西,但是任何线程需要在这个仓库取回东西的时候,绝对不会取到其它线程存放的东西。注意,这里将ThreadLocal比作仓库,只是方便我们理解,后面会分析通过ThreadLocal这个工具类,把东西到底存储到哪里了?

二、用法举例 -  上手很简单  

    为了对ThreadLocal有个初步的认识,我们还是通过具体的例子来说明。下面是ThreadLocal这个类的API,可以用set()方法和get()方法进行最基本的存值和取值操作。

   下面是一段代码:用一个主线程和其它二个线程分别往ThreadLocal存入变量User的实例对象,然后再取出并打印出来。

public class ThreadLocalTest {
	 
	
	private static ThreadLocal<User> userThreadLocal = new ThreadLocal<User>() {
		// 重写这个方法,可以修改“线程变量”的初始值,默认是null
		@Override
		protected User initialValue() {
			return new User("用户0");
		}
	};
	
	static class User {
	
		User(String name) {
			this.name = name;
		}
		String name;
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
		
	}
 
	public static void main(String[] args) {
		
		// 创建一号线程
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				// 在一号线程中将ThreadLocal变量设置为<用户1>
				User user1 = new User("用户1");
				userThreadLocal.set(user1);
				System.out.println("一号线程中ThreadLocal变量中保存的值为:" + userThreadLocal.get().getName());
				
			}
		}).start();
 
		// 创建二号线程
		new Thread(new Runnable() {
			@Override
			public void run() {
				
				// 在二号线程中将ThreadLocal变量设置为<用户2>
				User user2 = new User("用户2");
				userThreadLocal.set(user2);
				System.out.println("二号线程中ThreadLocal变量中保存的值为:" + userThreadLocal.get().getName());
			}
		}).start();
 
		//让一二号线程执行完毕,主线程休眠1000ms
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println("主线程中ThreadLocal变量中保存的值:" + userThreadLocal.get().getName());
	}
	
}

    输出结果如下:

一号线程中ThreadLocal变量中保存的值为:用户1
二号线程中ThreadLocal变量中保存的值为:用户2
主线程中ThreadLocal变量中保存的值:用户0

    程序输出结果可以看到:主线程输出的是<用户0>,如果这里不用TheadLocal,在一号线程和二号线程直接设置User变量的name分别为<用户1>和<用户2>,那么在一二号线程执行完毕后,由主线程打印这个变量,输出的值肯定是<user1>或者<user2>(输出哪一个由操作系统的线程调度先后顺序有关)。但使用ThreadLocal变量通过两个线程赋值后,在主线程程中输出的却是初始值<user0>。可以看到三个线程都往同一个ThreadLocal里面存入不同的值,但是取出来的时候,只需要一个简单的get()方法就能准确无误地拿到之前存入的值,这也就是ThreadLocal的核心作用:实现线程范围的局部变量,这也是为什么它可以称为「线程局部变量」。

三、原理分析 - 一探究竟

    通过上面的例子,我们已经初步了解了ThreadLocal的基本用法,我们现在分析下它的两个方法set()与get()的源代码,对ThreadLocal的原理一探究竟。

3.1 ThreadLocal.set() 方法:


    public void set(T value) {
        // 获取到当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 将值存入map, 这里的key是this,指向ThreadLocal对象
            map.set(this, value);
        else
            // 第一次调用就创建当前线程对应的Map并插入value
            createMap(t, value);
    }


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
public class Thread implements Runnable {

... 省略 ...

ThreadLocal.ThreadLocalMap threadLocals = null;

... 省略 ...

}

     上面的源码集合到一张图里看起更清晰:

    每一个线程内部都有一个ThreadLocal.ThreadLocalMap属性。所以,我们通过ThreadLocal.set()方法存入的值,并不由ThreadLocal自身来实现存储,而是通过每一个线程独立拥有的ThreadLocalMap对象来实现存储的。

    那么ThreadLocalMap又是什么呢?

  static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

     ... 省略其他代码 ...  
        
    }

    我们可以看到,ThreadLocalMap包含了一个继承了WeakReference(弱引用类)的Entry类,通过它来实现类似Map的<key, value>存储结构,我们可以简单地将ThreadLocalMap当成一个HashMap来使用,但是这里要注意的是,这里的key就是ThreadLocal对象,它是一个弱引用,这样设计是为了实现内存回收,这里为了避免复杂化,暂不深入分析,要理解「弱引用」可以参考:WeakReference。总之,目前只需要知道我们通过ThreadLocal存入的值,都存入到了Thread.TheadLocalMap的Entry里,键值KEY是TheadLocal。

    类的引用路径: 

    虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的。

3.2 ThreadLocacl.get() 方法:

public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //每个线程 都有一个自己的ThreadLocalMap,
    //ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

    通过上面的分析,再看下面这张图,是不是感觉很清晰了:

四、运用场景 - 事务管理

    通过上面的例子和分析,我们已经初步了解了ThreadLocal的基本用法和原理,但上面案例比较简单,还是需要引入更加实际的应用场景才能让我们体会到ThreadLocal的妙处。

    ThreadLocal在Spring中发挥着重要作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块中都出现了它的身影。 想要了解Spring事务管理的底层技术,必须要攻克ThreadLocal。

    ThreadLocal是用来处理多线程并发问题的一种解决方案。ThreadLocal是的作用是提供线程的局部变量,在多线程并发环境下,提供了与其他线程隔离的局部变量。通常这样的设计的情况是因为这个局部变量是不适合放在全局变量进行同步处理的。比如在事务管理中,在service类中的涉及到事务的方法,每个事务的上下文都应该是独立拥有数据库的connection连接的,否则在数据提交回滚过程中就会产生冲突。

4.1 构建场景

    我们先构建一个简单的转账场景:有个数据表account, 里面有两个用户Jack和Rose, 用户Jack给用户Rose转账。

    Dao层实现:通过JdbcUtil工具类获取数据数据源连接「connection」,实现转出「out」和转入「in」操作。

 

4.2 开启事务管理

    转出与转入两个步骤是一个转账操作,也称之为一个事务。事务的特性之一是要具备原子性。也就是说转入转出操作是一个整体,不可分割,要么全部成功,要不全部失败。如果执行过程中抛出异常,则回滚到事务执行前的初始状态。注意以下几点:

  • 每个事务的执行需要通过数据源连接池获取到数据库的connetion。
  • 为了保证所有的数据库操作都属于同一个事务,事务使用的连接必须是同一个,也就是说Service层开启事务的connection需要跟dao层访问数据库的connection保持一致。
  • 线程安全:在多个线程并发的情况下,每个线程都只能操作各自的connetion.

4.2.1 反例一

    可以看到service层和dao层的connection都是数据库连接池获取到的,两者并不是同一个connetion,并且在多线程并发的情况下,无法保证线程安全。

4.2.2 反例二

    通过加锁保证线程安全,通过传参来保证两个方法中connection的一致性。弊端:

1、降低了程序的性能,通过synchronized关键字加锁程序失去并发性,只能单个线程串行执行;

2、增加了代码耦合性,参数传递使service和dao层之间有了直接的联系。

4.2.3 正解

    通过修改JdbcUtils的工具类,我们引入ThreadLocal<Connecton>来实现,巧妙地将connection和线程绑定在一起,在多线程并发的情况下并不会产生错乱。可以看到在一些特定场景下,ThreadLocal有比较突出的优势:

1、传递数据:每个线程绑定的数据,在需要的地方可直接取出,避免直接传参造成代码耦合。

2、线程隔离:各线程之间数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

参考:

1. 通俗易懂讲解ThreadLocal​​​​​​

2. Spring JDBC-Spring事务管理之ThreadLocal基础知识

3. 透彻理解Spring事务设计思想之手写实现

4. ThreadLocal在Spring事务管理中的应用

猜你喜欢

转载自blog.csdn.net/crazestone0614/article/details/127619563