服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 详谈ThreadLocal-单例模式下高并发线程安全

详谈ThreadLocal-单例模式下高并发线程安全

2021-12-20 14:19牛麦康纳 Java教程

这篇文章主要介绍了ThreadLocal-单例模式下高并发线程安全,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

ThreadLocal-单例模式下高并发线程安全

在多例的情况下,每个对象在堆中声明内存空间,多线程对应的Java栈中的句柄或指针指向堆中不同的对象,对象各自变量的变更只会印象到对应的栈,也就是对应的线程中,不会影响到其它线程。所以多例的情况下不需要考虑线程安全的问题,因为一定是安全的。

详谈ThreadLocal-单例模式下高并发线程安全

而在单例的情况下却完全不一样了,在堆中只有一个对象,多线程对应的Java栈中的句柄或指针指向同一个对象,方法的参数变量和方法内变量是线程安全的,因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。但是成员变量只有一份,所有指向堆中该对象的句柄或指针都可以随时修改和读取它,所以是非线程安全的。

详谈ThreadLocal-单例模式下高并发线程安全

为了解决线程安全的问题,我们有3个思路:

  • 第一每个线程独享自己的操作对象,也就是多例,多例势必会带来堆内存占用、频繁GC、对象初始化性能开销等待等一些列问题。
  • 第二单例模式枷锁,典型的案例是HashTable和HashMap,对读取和变更的操作用synchronized限制起来,保证同一时间只有一个线程可以操作该对象。虽然解决了内存、回收、构造、初始化等问题,但是势必会因为锁竞争带来高并发下性能的下降。
  • 第三个思路就是今天重点推出的ThreadLocal。单例模式下通过某种机制维护成员变量不同线程的版本。
  • 假设三个人想从镜子中看自己,第一个方案就是每人发一个镜子互不干扰,第二个方案就是只有一个镜子,一个人站在镜子前其他人要排队等候,第三个方案就是我这里发明了一种“魔镜”,所有人站在镜子前可以并且只能看到自己!!!

主程序:

public static void main(String[] args) {
		//Mirror是个单例的,只构建了一个对象
		Mirror mirror = new Mirror("魔镜");
		
		//三个线程都在用这面镜子
		MirrorThread thread1 = new MirrorThread(mirror,"张三");
		MirrorThread thread2 = new MirrorThread(mirror,"李四");
		MirrorThread thread3 = new MirrorThread(mirror,"王二");
		
		thread1.start();
		thread2.start();
		thread3.start();
	}

很好理解,创建了一面镜子,3个人一起照镜子。

MirrorThread:

public class MirrorThread extends Thread{
	private Mirror mirror;	
	private String threadName;	
	public MirrorThread(Mirror mirror, String threadName){
		this.mirror = mirror;
		this.threadName = threadName;
	}
		
	//照镜子
	public String lookMirror() {
		return threadName+" looks like "+ mirror.getNowLookLike().get();
	}
	
	//化妆
	public void makeup(String makeupString) {
		mirror.getNowLookLike().set(makeupString);
	}
	
	@Override
  public void run() {
		int i = 1;//阈值
		while(i<5) {
			try {
				long nowFace = (long)(Math.random()*5000);
				sleep(nowFace);
				StringBuffer sb = new StringBuffer();
				sb.append("第"+i+"轮从");
				sb.append(lookMirror());
				makeup(String.valueOf(nowFace));
				sb.append("变为");
				sb.append(lookMirror());
				System.out.println(sb);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			i++;
		}
	}
}

也很好理解,就是不断的更新自己的外貌同时从镜子里读取自己的外貌。

重点是Mirror:

public class Mirror {
	private String mirrorName;	
	//每个人要看到自己的样子,所以这里要用ThreadLocal
	private ThreadLocal<String> nowLookLike;	
	public Mirror(String mirrorName){
		this.mirrorName=mirrorName;
		nowLookLike = new ThreadLocal<String>();
	}

	public String getMirrorName() {
		return mirrorName;
	}

	public ThreadLocal<String> getNowLookLike() {
		return nowLookLike;
	}
}

对每个人长的样子用ThreadLocal类型来表示。

先看测试结果:

第1轮从张三 looks like null变为张三 looks like 3008

第2轮从张三 looks like 3008变为张三 looks like 490

第1轮从王二 looks like null变为王二 looks like 3982

第1轮从李四 looks like null变为李四 looks like 4390

第2轮从王二 looks like 3982变为王二 looks like 1415

第2轮从李四 looks like 4390变为李四 looks like 1255

第3轮从王二 looks like 1415变为王二 looks like 758

第3轮从张三 looks like 490变为张三 looks like 2746

第3轮从李四 looks like 1255变为李四 looks like 845

第4轮从李四 looks like 845变为李四 looks like 1123

第4轮从张三 looks like 2746变为张三 looks like 2126

第4轮从王二 looks like 758变为王二 looks like 4516

OK,一面镜子所有人一起照,而且每个人都只能看的到自己的变化,这就达成了单例线程安全的目的。

我们来细看下它是怎么实现的。

先来看Thread:

Thread中维护了一个ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap这个Map的key是ThreadLocal,value是维护的成员变量。现在的跟踪链是Thread->ThreadLocalMap-><ThreadLocal,Object>,那么我们只要搞明白Thread怎么跟ThreadLocal关联的,从线程里找到自己关心的成员变量的快照这条线就通了。

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);
      }

再来看ThreadLocal:它里面核心方法两个get()和set(T)

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();
  }
public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

方法里通过Thread.currentThread()的方法得到当前线程,然后做为key存储到当前线程对象的threadLocals中,也就是TreadLocalMap中。

OK,这样整个关系链已经建立,真正要去访问的成员变量在一个map中,key是线程号,值是属于该线程的快照。

ThreadLocal里还有map的创建createMap(t, value)、取值时对象的初始值setInitialValue()、线程结束时对象的释放remove()等细节,有兴趣的可以继续跟进了解下。

ThreadLocal应用其实很多,例如Spring容器中实例默认是单例的,transactionManager也一样,那么事务在处理时单例的manager是如何控制每个线程的事务要如何处理呢,这里面就应用了大量的ThreadLocal。

 

多线程中的ThreadLocal

1.ThreadLocal概述

多线程的并发问题主要存在于多个线程对于同一个变量进行修改产生的数据不一致的问题,同一个变量指的值同一个对象的成员变量或者是同一个类的静态变量。之前我们常听过尽量不要使用静态变量,会引起并发问题,那么随着Spring框架的深入人心,单例中的成员变量也出现了多线程并发问题。Struts2接受参数采用成员变量自动封装,为此在Spring的配置采用多例模式,而SpringMVC将Spring的容器化发挥到极致,将接受的参数放到了注解和方法的参数中,从而避免了单例出现的线程问题。今天,我们讨论的是JDK从1.2就出现的一个并发工具类ThreadLocal,他除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。我们先看一下官方是怎么解释这个变量的?

详谈ThreadLocal-单例模式下高并发线程安全

详谈ThreadLocal-单例模式下高并发线程安全

大致意思是:此类提供了局部变量表。这些变量与普通变量不同不同之处是,每一个通过get或者set方法访问一个线程都是他自己的,将变量的副本独立初始化。ThreadLocal实例通常作用于希望将状态与线程关联的类中的私有静态字段(例如,用户ID或交易ID)。

只要线程是活动的并且可以访问{@code ThreadLocal}实例, 每个线程都会对其线程局部变量的副本保留隐式引用。 线程消失后,其线程本地实例的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)。也就是说,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。而每个线程的副本全部放到ThreadLocalMap中。

2. ThreadLocal简单实用

public class ThreadLocalExample {
  public static class MyRunnable implements Runnable {
      private ThreadLocal<Double> threadLocal = new ThreadLocal();
      private Double variable;

      @Override
      public void run() {
          threadLocal.set(Math.floor(Math.random() * 100D));
          variable = Math.floor(Math.random() * 100D);
          try {
              Thread.sleep(2000);
          } catch (InterruptedException e) {
          }

          System.out.println("ThreadValue==>"+threadLocal.get());
          System.out.println("Variable==>"+variable);
      }
  }
  
  public static void main(String[] args) {
      MyRunnable sharedRunnableInstance = new MyRunnable();
      Thread thread1 = new Thread(sharedRunnableInstance);
      Thread thread2 = new Thread(sharedRunnableInstance);
      thread1.start();
      thread2.start();
  } 
}

详谈ThreadLocal-单例模式下高并发线程安全

通过上面的例子,我们发现将Double放入ThreadLocal中,不会出现多线程并发问题,而成员变量variable却发生了多线程并发问题。

3.ThreadLocal的内部原理

通过源码我们发现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.
* 
* 返回此线程局部变量的当前线程副本中的值。
* 如果该变量没有当前线程的值,则首先将其初始化为调用{@link #initialValue}方法返回的值。
* @return the current thread's value of this thread-local
*/
public T get() { }

/**
* 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.
*
* 将此线程局部变量的当前线程副本设置为指定值。
* 大多数子类将不需要重写此方法,而仅依靠{@link #initialValue}方法来设置线程局部变量的值。
*
* @param value the value to be stored in the current thread's copy of
*              this thread-local.
*              要存储在此本地线程的当前线程副本中的值。
*/
public void set(T value) { }

/**
* Removes the current thread's value for this thread-local
* variable.  If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim.  This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
* 删除此线程局部变量的当前线程值。
* 如果此线程局部变量随后被当前线程{@linkplain #get read}调用,
* 则其值将通过调用其{@link #initialValue}方法来重新初始化,
* 除非当前值是在此期间被设置{@linkplain #set set}。
* 这可能会导致在当前线程中多次调用{@code initialValue}方法。
* @since 1.5
*/
public void remove() { }

/**
* Returns the current thread's "initial value" for this
* thread-local variable.  This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread.  Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
* 返回此线程局部变量的当前线程的“初始值”。
* 除非线程先前调用了{@link #set}方法,
* 否则线程第一次使用{@link #get}方法访问该变量时将调用此方法,
* 在这种情况下,{@ code initialValue}方法将不会为线程被调用。
* 通常,每个线程最多调用一次此方法,
* 但是在随后调用{@link #remove}之后再调用{@link #get}的情况下,可以再次调用此方法。
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden.  Typically, an
* anonymous inner class will be used.
* 此实现仅返回{@code null};如果程序员希望线程局部变量的初始值不是{@code null},
* 则必须将{@code ThreadLocal}子类化,并重写此方法。通常,将使用匿名内部类。
*
* @return the initial value for this thread-local
*/
protected T initialValue(){ }

3.1 get方法

public T get() {
  //获取当前线程
  Thread t = Thread.currentThread();
  //通过当前线程获取ThreadLocalMap
  //Thread类中包含一个ThreadLocalMap的成员变量
  ThreadLocalMap map = getMap(t);
  //如果不为空,则通过ThreadLocalMap中获取对应value值
  if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
      }
  }
  //如果为空,需要初始化值
  return setInitialValue();
}
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;
}

首先是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。 如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

在setInitialValue方法中,首先执行了initialValue方法(我们上面提到的最后一个方法),接着通过当前线程获取ThreadLocalMap,如果不存在则创建。创建的代码很简单,只是通过ThreadLocal对象和设置的Value值创建ThreadLocalMap对象。

3.2 set方法

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

这个方法和setInitialValue方法的业务逻辑基本相同,只不过setInitialValue调用了initialValue()的钩子方法。这里代码简单,我们就不做过多解释。

3.3 remove方法

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

这个方法是从jdk1.5才出现的。处理逻辑也很很简单。通过当前线程获取到ThreadLocalMap对象,然后移除此ThreadLocal。

3.4 initialValue方法

protected T initialValue() {
  return null;
}

是不是感觉简单了,什么也没有处理,直接返回一个null,那么何必如此设计呢?当我们发现他的修饰符就会发现,他应该是一个钩子方法,主要用于提供子类实现的。追溯到源码中我们发现,Supplier的影子,这就是和jdk8的lamda表达式关联上了。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
  private final Supplier<? extends T> supplier; 
  SuppliedThreadLocal(Supplier<? extends T> supplier) {
      this.supplier = Objects.requireNonNull(supplier);
  }
  
  @Override
  protected T initialValue() {
      return supplier.get();
  }
}

4. 总结

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。之所以这里是一个map,是因为通过线程会存在多个类中定义ThreadLocal的成员变量。初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals; 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

5. ThreadLocalMap引发的内存泄漏

ThreadLocal属于一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量。下面我们来看看ThreadLocalMap这个类的一个entry:

static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object val
  Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
  }
}

public WeakReference(T referent) {
  super(referent); //referent:ThreadLocal的引用
}

//Reference构造方法     
Reference(T referent) {
  this(referent, null);//referent:ThreadLocal的引用
}

Reference(T referent, ReferenceQueue<? super T> queue) {
  this.referent = referent;
  this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。

原文链接:https://yejingtao.blog.csdn.net/article/details/78806902

延伸 · 阅读

精彩推荐