SpringBoot——使用ThreadLocal解决类成员变量并发线程安全问题!

问题

  在开发过程中,我们一旦在某个类中使用一个可变的成员变量,就会涉及到线程安全问题,因为我们的类对于其他依赖使用类来说,可能是单例注入的,这就会涉及到多个线程共享操作同一个变量问题。如何解决?
  遇到线程安全问题,我们首先想到的就是使用,万物可加锁,只要不怕慢!我们通过加锁来实现多个线程并发访问操作问题,我加锁,你就得等我解锁后才能操作。但是众所周知,加锁,必定会在多线程并发访问时造成一部分线程阻塞等待,从而产生一定的性能影响。那除了加锁,有没有其他方法来避免?答案是:有滴!我们可以使用多种方式,下面我们娓娓道来~

ThreadLocal方式

介绍

  1. ThreadLocal从字面理解就是本地线程,全称:Thread Local Variable。换句话说,就是当前线程变量,它是一个本地线程变量,其填充的是当前线程的变量,这个变量对于其他线程来说都是封闭且隔离的
  2. 如何实现变量隔离这一功能?ThreadLocal可以为每个线程创建一个自有副本,每个线程可以访问自己内部的副本变量来达到隔离效果,从而解决共享变量的线程安全问题。
  3. ThreadLocal变量是线程内部的局部变量,在不同的线程Thread中有不同的副本,副本只能由当前Thread使用,不存在多线程共享问题。
  4. ThreadLocal一般由private static修饰,线程结束时,可回收掉ThreadLocal副本。

案例

之前在SpringBoot—集成AOP详解(面向切面编程Aspect)中的AOP编码中也是用到了ThreadLocal进行starttime变量的存储。

源码

set方法


    /**
     * 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();
        //获取当前线程中的变量map
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        	//若为空,则初始化当前线程的变量map,key为当前线程,map为变量
            createMap(t, value);
    }
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

 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.
         */
        //弱引用,若为null时,ThreadLocal被回收,但是map的value还存在,容易造成内存泄漏
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
			//使用Entry保存数据,k为ThreadLocal,v为变量value值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

	 	...
	 	//其他源码省略
	 	...

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
   }
  1. ThreadLocalMapThreadLocal的一个静态内部类,使用Entry保存数据。
  2. Entry继承WeakReference弱引用,key为当前线程ThreadLocal,value为变量值。

get方法


    /**
     * 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();
        //获取变量map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //从获取Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

内存泄漏问题

原因

  1. ThreadLocal是弱引用,若为null时,ThreadLocal被回收(这样可以避免Entry内存泄漏)。
  2. 虽然ThreadLocalMap保存的ThreadLocal弱引用被回收了,但的value还存在,容易造成内存泄漏。

引用

  • 强引用:强引用的对象,不会被回收。如直接new一个对象,就算OOM异常,也不会回收该对象。
  • 软引用:软引用的对象,只有发生gc时,发现内存不足,才会回收。如缓存,系统内存充足时,一般不会回收对象,当系统内存不足且gc时,就会回收这些软引用对象。
  • 弱引用:弱引用的对象,只要发生gc,就会被回收。
  • 虚引用:虚引用一般和引用队列联合使用,对象若持有虚引用,就等于没有任何引用,在任何时候都可能被gc,主要用来跟踪对象被gc的活动。

解决方案

在使用完ThreadLocal的时候,最后使用remove()方法进行当前线程变量值的移除。

使用场景

  1. 线程间数据隔离,每个线程创建自己的ThreadLocal变量副本。
  2. 进行事务操作,用于存储线程事务信息。
  3. 数据库连接,进行Session会话管理。
  4. 解决多线程中数据并发不一致的问题。

ThreadLocal和synchronized区别

  1. synchronized是基于锁机制,在某一时刻,变量或者代码只能由一个线程进行访问,有上锁和解锁的边界,用于解决多个线程之间的数据共享竞争问题。
  2. ThreadLocal是基于每个线程有独立的变量副本,每个线程在同一时刻都可以访问变量,可并发访问,只不过多个线程访问到的变量不是同一个,是各自线程内独立的副本,用于解决多个线程需隔离数据共享的问题。

使用方式

代码示例

    public static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args) {
    	try {
        LongStream.range(100000000, 100000005)
                .forEach(a -> new Thread(()-> {
                    local.set(Thread.currentThread().getName() + "-" + a);
            System.out.println("线程名称:" + Thread.currentThread().getName() + ", local: " + local.get());
        }).start());
        } finally {
              local.remove();
		}


    }

运行结果

线程名称:Thread-0, local: Thread-0-100000000
线程名称:Thread-3, local: Thread-3-100000003
线程名称:Thread-2, local: Thread-2-100000002
线程名称:Thread-1, local: Thread-1-100000001
线程名称:Thread-4, local: Thread-4-100000004

  我们可以从运行结果中看出,local是跟随Thread独立的。

@Scope多例注解方式

介绍

  1. 使用@Scope("prototype")注解,解决Bean的多例问题,替代性的解决多线程类成员变量共享问题。
  2. 在使用Spring的IOC功能来管理Bean时,默认是单例的,在多线程下,类的成员变量如果是个可变的值,则会有线程安全问题。需要的时候,我们可以直接拿来即用,使用@Autowired@Resource注解注入即可。

使用

  1. Service层使用格式
@Service
@Scope("prototype")
public class XxxService {

}
  1. 上层注入使用
    需要区分上层是否也会是单例。建议使用@Resource注解注入。
posted @ 2023-04-14 09:45  Andya_net  阅读(1141)  评论(0编辑  收藏  举报  来源