Java多线程(4):ThreadLocal

为提高CPU利用率,工程师创造多线程和线程池。ThreadLocal核心思想是将资源拷贝成多份副本同时访问,实现空间换时间。通过代码对比不同资源连接方式,表明ThreadLocal可实现线程资源复用及线程间通信,但在应用级开发中使用较少,易造成OOM。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

您好,我是湘王,这是我的优快云博客,欢迎您来,欢迎您再来~

为了提高CPU的利用率,工程师们创造了多线程。但是线程们说:要有光!(为了减少线程创建(T1启动)和销毁(T3切换)的时间),于是工程师们又接着创造了线程池ThreadPool。就这样就可以了吗?——不,工程师们并不满足于此,他们不把自己创造出来的线程给扒个底朝天决不罢手。

有了线程关键字解决线程安全问题,有了线程池解决效率问题,那还有什么问题是可以需要被解决的呢?——还真被这帮疯子攻城狮给找到了!

当多个线程共享同一个资源的时候,为了保证线程安全,有时不得不给资源加锁,例如使用Synchronized关键字实现同步锁。这本质上其实是一种时间换空间的搞法——用单一资源让不同的线程依次访问,从而实现内容安全可控。就像这样:

但是,可以不可以反过来,将资源拷贝成多份副本的形式来同时访问,达到一种空间换时间的效果呢?当然可以,就像这样:

而这,就是ThreadLocal最核心的思想。

但这种方式在很多应用级开发的场景中用得真心不多,而且有些公司还禁止使用ThreadLocal,因为它搞不好还会带来一些负面影响。

其实,从拷贝若干副本这种功能来看,ThreadLocal是实现了在线程内部存储数据的能力的,而且相互之间还能通信。就像这样:

还是以代码的形式来解读一下ThreadLocal。有一个资源类Resource:

/**
 * 资源类
 *
 * @author 湘王
 */
public class Resource {
	private String name;
	private String value;

	public Resource(String name, String value) {
		super();
		this.name = name;
		this.value = value;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}
}

分别有ResuorceUtils1、ResuorceUtils2和ResuorceUtils3分别以不同的方式来连接资源,那么看看效率如何。

/**
 * 连接资源工具类,通过静态方式获得连接
 *
 * @author 湘王
 */
public class ResourceUtils1 {
	// 定义一个静态连接资源
	private static Resource resource = null;
	// 获取连接资源
	public static Resource getResource() {
		if(resource == null) {
			resource = new Resource("xiangwang", "123456");
		}
		return resource;
	}

	// 关闭连接资源
	public static void closeResource() {
		if(resource != null) {
			resource = null;
		}
	}
}



/**
 * 连接资源工具类,通过实例化方式获得连接
 *
 * @author 湘王
 */
public class ResourceUtils2 {
	// 定义一个连接资源
	private Resource resource = null;
	// 获取连接资源
	public Resource getResource() {
		if(resource == null) {
			resource = new Resource("xiangwang", "123456");
		}
		return resource;
	}

	// 关闭连接资源
	public void closeResource() {
		if(resource != null) {
			resource = null;
		}
	}
}



/**
 * 连接资源工具类,通过线程中的static Connection的副本方式获得连接
 *
 * @author 湘王
 */
public class ResourceUtils3 {
	// 定义一个静态连接资源
	private static Resource resource = null;
	private static ThreadLocal<Resource> resourceContainer = new ThreadLocal<Resource>();
	// 获取连接资源
	public static Resource getResource() {
		synchronized(ResourceManager.class) {
			resource = resourceContainer.get();
			if(resource == null) {
				resource = new Resource("xiangwang", "123456");
				resourceContainer.set(resource);
			}
			return resource;
		}
	}

	// 关闭连接资源
	public static void closeResource() {
		if(resource != null) {
			resource = null;
			resourceContainer.remove();
		}
	}
}



/**
 * 连接资源管理类
 *
 * @author 湘王
 */
public class ResourceManager {
	public void insert() {
		// 获取连接
		// System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
		// Resource resource = new ResourceUtils2().getResource();
		Resource resource = ResourceUtils3.getResource();
		System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + resource);
	}

	public void delete() {
		// 获取连接
		// System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
		// Resource resource = new ResourceUtils2().getResource();
		Resource resource = ResourceUtils3.getResource();
		System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + resource);
	}

	public void update() {
		// 获取连接
		// System.out.println("Dao.update()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
		// Resource resource = new ResourceUtils2().getResource();
		Resource resource = ResourceUtils3.getResource();
		System.out.println("Dao.update()-->" + Thread.currentThread().getName() + resource);
	}

	public void select() {
		// 获取连接
		// System.out.println("Dao.select()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
		// Resource resource = new ResourceUtils2().getResource();
		Resource resource = ResourceUtils3.getResource();
		System.out.println("Dao.select()-->" + Thread.currentThread().getName() + resource);
	}

	public void close() {
		ResourceUtils3.closeResource();
	}

	public static void main(String[] args) {
		for (int i = 0; i < 3; i++) {
			new Thread(new Runnable() {
				ResourceManager rm = new ResourceManager();
				@Override
				public void run() {
					rm.insert();
					rm.delete();
					rm.update();
					rm.select();
					rm.close();
				}
			}).start();
		}
	}
}

执行ResourceManager类中的main()方法后,可以清楚地看到:

第一种静态方式:大部分资源都能复用,但毫无规律;

第二种实例方式:即使是同一个线程,资源实例也不一样;

第三种ThreadLocal静态方式:相同的线程有相同的实例。

结论是:ThreadLocal实现了线程的资源复用。

也可以通过画图的方式来看清楚三者之间的不同:

这是静态方式下的资源管理:

这是实例方式下的资源管理:

这是ThreadLocal静态方式下的资源管理:

理解了之后,再来看一个数据传递的例子,也就是ThreadLocal实现线程间通信的例子:

/**
 * 数据传递
 *
 * @author 湘王
 */
public class DataDeliver {
	static class Data1 {
		public void process() {
			Resource resource = new Resource("xiangwang", "123456");
			//将对象存储到ThreadLocal
			ResourceContextHolder.holder.set(resource);
			new Data2().process();
		}
	}

	static class Data2 {
		public void process() {
			Resource resource = ResourceContextHolder.holder.get();
			System.out.println("Data2拿到数据: " + resource.getName());
			new Data3().process();
		}
	}

	static class Data3 {
		public void process() {
			Resource resource = ResourceContextHolder.holder.get();
			System.out.println("Data3拿到数据: " + resource.getName());
		}
	}

	static class ResourceContextHolder {
		public static ThreadLocal<Resource> holder = new ThreadLocal<>();
	}

	public static void main(String[] args) {
		new Data1().process();
	}
}

运行代码之后,可以看到Data1的数据都被Data2和Data3拿到了,就像这样:

ThreadLocal在实际应用级开发中较少使用,因为容易造成OOM:

1、由于ThreadLocal是一个弱引用(WeakReference<ThreadLocal<?>>),因此会很容易被GC回收;

2、但ThreadLocalMap的生命周期和Thread相同,这就会造成当key=null时,value却还存在,造成内存泄漏。所以,使用完ThreadLocal后需要显式调用remove操作(但很多码农不知道这一点)。


感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值