30. 线程本地存储模式:没有共享,就没有伤害 - 并发设计模式 -待完善


多个线程同时读写同一共享变量存在并发问题, 避免共享,就能避免并发问题。

1. ThreadLocal的使用方法

	static class ThreadId {
		static final AtomicLong nextId = new AtomicLong(0);
		// 定义ThreadLocal变量
		static final ThreadLocal<Long> tl = 
			ThreadLocal.withInitial(() -> nextId.getAndIncrement());

		// 此方法会为每个线程分配一个唯一的Id
		static long get() {
			return tl.get();
		}
	}
  • 一个线程前后两次调用ThreadId的get()方法,两次的返回值是相同的;
  • 两个线程分别调用ThreadId的get()方法,那么两个线程看到的get()方法的返回值是不同的。

SimpleDateFormat不是线程安全的,在并发场景下可以用ThreadLocal解决线程安全问题。不同线程并不共享SimpleDateFormat,就像局部变量一样,所以线程安全。

static class SafeDateFormat {
	// 定义ThreadLocal变量
	static final ThreadLocal<DateFormat> tl = ThreadLocal
			.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

	static DateFormat get() {
		return tl.get();
	}
}

2. ThreadLocal的工作原理

2.1 自己实现的ThreadLocal

在这里插入图片描述

class MyThreadLocal<T> {
	Map<Thread, T> locals = new ConcurrentHashMap<>();

	// 获取线程变量
	T get() {
		return locals.get(Thread.currentThread());
	}

	// 设置线程变量
	void set(T t) {
		locals.put(Thread.currentThread(), t);
	}
}

直接创建一个Map,线程为key,线程拥有的变量为value。

2.2 java实现ThreadLocal

在这里插入图片描述

class Thread {
	// 内部持有ThreadLocalMap
	ThreadLocal.ThreadLocalMap threadLocals;
}

class ThreadLocal<T> {
	public T get() {
		// 首先获取线程持有的
		// ThreadLocalMap
		ThreadLocalMap map = Thread.currentThread().threadLocals;
		// 在ThreadLocalMap中
		// 查找变量
		Entry e = map.getEntry(this);
		return e.value;
	}

	static class ThreadLocalMap {
		// 内部是数组而不是Map
		Entry[] table;

		// 根据ThreadLocal查找Entry
		Entry getEntry(ThreadLocal key) {
			// 省略查找逻辑
		}

		// Entry定义
		static class Entry extends WeakReference<ThreadLocal> {
			Object value;
		}
	}
}

Java的实现里面也有一个Map,叫做ThreadLocalMap,不过持有ThreadLocalMap的不是ThreadLocal,而是Thread。Thread这个类内部有一个私有属性threadLocals,其类型就是ThreadLocalMap,ThreadLocalMap的Key是ThreadLocal。

我们的设计里面Map属于ThreadLocal,而Java的实现里面ThreadLocalMap则是属于Thread。这两种方式哪种更合理呢?很显然Java的实现更合理一些。在Java的实现方案里面,ThreadLocal仅仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在Thread里面,这样的设计容易理解。而从数据的亲缘性上来讲,ThreadLocalMap属于Thread也更加合理

还有一个原因,不容易产生内存泄露. 在我们的设计方案中,ThreadLocal持有的Map会持有Thread对象的引用,这就意味着,只要ThreadLocal对象存在,那么Map中的Thread对象就永远不会被回收。ThreadLocal的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而Java的实现中Thread持有ThreadLocalMap,而且ThreadLocalMap里对ThreadLocal的引用还是弱引用(WeakReference),所以只要Thread对象可以被回收,那么ThreadLocalMap就能被回收。Java的这种实现方案虽然看上去复杂一些,但是更加安全。

3. ThreadLocal与内存泄露

在线程池中使用ThreadLocal可能导致内存泄露,
// TODO 作者写的不是很好理解,待补

4. InheritableThreadLocal与继承性

不建议使用。

5. 总结

线程本地存储模式本质上是一种避免共享的方案。

6. 课后思考

实际工作中,有很多平台型的技术方案都是采用ThreadLocal来传递一些上下文信息,例如Spring使用ThreadLocal来传递事务信息。我们曾经说过,异步编程已经很成熟了,那你觉得在异步场景中,是否可以使用Spring的事务管理器呢?

发布了97 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39530821/article/details/102773357