Java并发编程:ThreadLocal的使用以及实现原理解析
前言
前面的文章里,我们学习了有关锁的使用,锁的机制是保证同一时刻只能有一个线程访问临界区的资源,也就是通过控制资源的手段来保证线程安全,这固然是一种有效的手段,但程序的运行效率也因此大大降低。那么,有没有更好的方式呢?答案是有的,既然锁是严格控制资源的方式来保证线程安全,那我们可以反其道而行之,增加更多资源,保证每个线程都能得到所需对象,各自为营,互不影响,从而达到线程安全的目的,而ThreadLocal便是采用这样的思路。
ThreadLocal实例
ThreadLocal翻译成中文的话大概可以说是:线程局部变量,也就是只有当前线程能够访问。它的设计作用是为每一个使用该变量的线程都提供一个变量值的副本,每个线程都是改变自己的副本并且不会和其他线程的副本冲突,这样一来,从线程的角度来看,就好像每个线程都拥有了该变量。
下面是一个简单的实例:
public class ThreadLocalDemo {
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0;i<3;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int value = local.get();
System.out.println(Thread.currentThread().getName() + ":" + value);
local.set(value + 1);
}
}
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
}
上面的代码不难理解,首先是定义了一个名为 local 的ThreadLocal变量,并初识变量的值为0,然后是定义了一个实现Runnable接口的内部类,在其run方法中对local 的值做读取和加1的操作,最后是main方法中开启两个线程来运行内部类实例。
以上就是代码的大概逻辑,运行main函数后,程序的输出结果如下:
Thread-0:0
Thread-1:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2
从结果可以看出,虽然两个线程都共用一个Runnable实例,但两个线程中所展示的ThreadLocal的数据值并不会相互影响,也就是说这种情况下的local 变量保存的数据相当于是线程安全的,只能被当前线程访问。
ThreadLocal实现原理
那么ThreadLocal内部是怎么保证对象是线程私有的呢?毫无疑问,答案需要从源码中查找。回顾前面的代码,可以发现其中调用了ThreadLocal的两个方法set 和 get,我们就从这两个方法入手。
先看 set() 的源码:
public void set(T value) {
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap,返回map
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//map为空,创建
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set的代码逻辑比较简单,主要是把值设置到当前线程的一个ThreadLocalMap对象中,而ThreadLocalMap可以理解成一个Map,它是定义在Thread类中内部的成员,初始化是为null,
ThreadLocal.ThreadLocalMap threadLocals = null;
不过,与常见的Map实现类,如HashMap之类的不同的是,ThreadLocalMap中的Entry是继承于WeakReference类的,保持了对 “键” 的弱引用和对 “值” 的强引用,这是类的源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//省略剩下的源码
....................
}
从源码中中可以看出,Entry构造函数中的参数 k 就是ThreadLocal实例,调用super(k) 表明对 k 是弱引用,使用弱引用的原因在于,当没有强引用指向 ThreadLocal 实例时,它可被回收,从而避免内存泄露,那么为何需要防止内存泄露呢?原因下面会说到。
接着说set方法的逻辑,当调用set方法时,其实是将数据写入threadLocals这个Map对象中,这个Map的key为ThreadLocal当前对象,value就是我们存入的值。而threadLocals本身能保存多个ThreadLocal对象,相当于一个ThreadLocal集合。
接着看 get() 的源码:
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;
}
}
//设置初识值到ThreadLocal中并返回
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;
}
get方法的逻辑也是比较简单的,就是直接获取当前线程的ThreadLocalMap对象,如果该对象不为空就返回它的value值,否则就把初始值设置到ThreadLocal中并返回。
看到这,我们大概就能明白为什么ThreadLocal能实现线程私有的原理了,其实就是每个线程都维护着一个ThreadLocal的容器,这个容器就是ThreadLocalMap,可以保存多个ThreadLocal对象。而调用ThreadLocal的set或get方法其实就是对当前线程的ThreadLocal变量操作,与其他线程是分开的,所以才能保证线程私有,也就不存在线程安全的问题了。
然而,该方案虽然能保证线程私有,但却会占用大量的内存,因为每个线程都维护着一个Map,当访问某个ThreadLocal变量后,线程会在自己的Map内维护该ThreadLocal变量与具体实现的映射,如果这些映射一直存在,就表明ThreadLocal 存在引用的情况,那么系统GC就无法回收这些变量,可能会造成内存泄露。
针对这种情况,上面所说的ThreadLocalMap中Entry的弱引用就起作用了。
TheadLocal与同步机制的区别
最后,总结一下ThreadLocal和同步机制之间的区别吧。
实现机制:
同步机制采用了“以时间换空间”的方式,控制资源保证同一时刻只能有一个线程访问。
ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响,但因为每个线程都维护着一份副本,对内存空间的占用会增加。
数据共享:
同步机制是对公共资源做控制访问的方式来保证线程安全,但资源仍是共享状态,可用于线程间的通信;
ThreadLocal是每个线程都有自己的资源(变量)副本,互相之间不影响,也就不存在共享的说法了。
Java并发编程:ThreadLocal的使用以及实现原理解析的更多相关文章
- 【Java并发编程】24、Synchronized实现原理解析
一.概述 我们知道在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它. 不过,随着后续Java版本更新对 ...
- Java 并发编程:volatile的使用及其原理
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- Java并发编程-阻塞队列(BlockingQueue)的实现原理
背景:总结JUC下面的阻塞队列的实现,很方便写生产者消费者模式. 常用操作方法 常用的实现类 ArrayBlockingQueue DelayQueue LinkedBlockingQueue Pri ...
- Java 并发编程——Executor框架和线程池原理
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- Java并发编程之深入理解线程池原理及实现
Java线程池在实际的应用开发中十分广泛.虽然Java1.5之后在JUC包中提供了内置线程池可以拿来就用,但是这之前仍有许多老的应用和系统是需要程序员自己开发的.因此,基于线程池的需求背景.技术要求了 ...
- 【转】Java 并发编程:volatile的使用及其原理
一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果 ...
- Java 并发编程:volatile的使用及其原理(二)
一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果 ...
- JAVA并发编程:相关概念及VOLATILE关键字解析
一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...
- 浅谈Java并发编程系列(八)—— LockSupport原理剖析
LockSupport 用法简介 LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现. LockSupport是用来创建锁和其他同步 ...
随机推荐
- Shell脚本-基本运算符
跟着RUNOOB网站的教程学习的笔记 shell和其他编程语言一样,支持多种运算符,包括: 算术运算符 关系运算符 布尔运算符 字符串运算符 文件测试运算符 expr是一款表达式计算公式,使用它能完成 ...
- java批量将多文件打包成zip格式
public void createzip(){ List<File> nFileList = new ArrayList<File>(); nFileList.add(new ...
- python信息收集之子域名
python信息收集之子域名 主要是以下3种思路: 字典爆破 搜索引擎 第三方网站 0x00 背景知识 list Python内置的一种数据类型是列表:list是一种有序的集合. >>&g ...
- java web 测试
1.功能测试 bugfree mantis 2.性能测试:loadrunner 3.安全测试: burpsuite
- WPF常用样式总结
常用控件样式: <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation ...
- 部署GitLab代码托管仓库
GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,GitLab是使用Ryby开发的一个开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私 ...
- JS跨域调用之document.domain--相同基础域名页面之间的调用
利用document.domain 实现跨域: 前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域 Javascript出于对 ...
- Struts2再爆远程命令执行漏洞![W3bSafe]Struts2-048 Poc Shell及防御修复方案抢先看!
漏洞概述 Apache Struts是美国阿帕奇(Apache)软件基金会负责维护的一个开源项目,是一套用于创建企业级Java Web应用的开源MVC框架.在Struts 2.3.x 系列的 Show ...
- Swift5 语言指南(三) 快速之旅
传统表明,新语言中的第一个程序应在屏幕上打印“Hello,world!”字样.在Swift中,这可以在一行中完成: print("Hello, world!") // Prints ...
- ELK实战(Springboot日志输出查找)
需求 把分布式系统,集群日志集中处理快速查询 搭建ELK并与springboot日志输出结合 搭建ELK 基于我前面的elasticsearch搭建博客文档docker-compose.yml基础上进 ...