Java多线程-ThreadLocal

1、ThreadLocal是什么?

  • 提供线程内局部变量,不同线程之间不会相互干扰。
  • ThreadLocal 实例通常来说都是 private static 修饰的,用于关联线程和线程的上下文。
  • 减少同一个线程内的函数 或 组件之间传递变量的复杂性

小结:

1
2
3
1. 线程并发:在多线程并发的场景
2. 传递数据:通过ThreadLocal在同一线程不同组件中传递公共变量。
3. 线程隔离:每个线程的变量都是独立的,不会互相影响

1.1、举例-线程隔离

  • 不使用ThreadLocal

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Demo {

    private String content;
    private void setContent(String content) {
    this.content= content;
    }
    private String getContent() {
    return content;
    }
    public static void main(String[] args) {
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.setContent(Thread.currentThread().getName() + "的数据");
    System.out.println("------------");
    System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
    }
    }, "线程" + i).start();
    }
    }
    }

    image-20230722155750292

  • 使用ThreadLocal

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Demo {
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private String content;
    private void setContent(String content) {
    threadLocal.set(content);
    }
    private String getContent() {
    return threadLocal.get();
    }
    public static void main(String[] args) {
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    demo.setContent(Thread.currentThread().getName() + "的数据");
    System.out.println("------------");
    System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
    }
    }, "线程" + i).start();
    }
    }
    }

    image-20230722155708884

1.2、对比synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class synchronizedDemo {
private String content;
private void setContent(String content) {
this.content= content;
}
private String getContent() {
return content;
}
public static void main(String[] args) {
synchronizedDemo demo = new synchronizedDemo();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (synchronizedDemo.class) {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("------------");
System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
}
}
}, "线程" + i).start();
}
}
}

image-20230722161534672

虽然 ThreadLocalSynchronized 关键字都是用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。

Synchronized ThreadLocal
原理 同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问。 采用 “以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而不互相干扰。
侧重点 多个线程之间访问资源的同步 并发情况下让每个线程之间数据相互隔离

1.3、ThreadLocal的好处

  1. 传递数据:保证每个线程保定的数据在需要的地方可以直接使用,这样避免了进行参数传递而带来的代码耦合问题。
  2. 线程隔离:各个线程之间的数据相互隔离但有具备并发性,同事避免了使用synchronized加锁带来的性能损耗问题。

2、案例

img

那么可以看到在service到Dao层的时候,都会使用connection,那么此时将connection对象和当前线程进行绑定,这样就能保证数据的一致性,并且避免传参导致的代码耦合问题。

2.1、Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {

public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();

try {
Connection conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出 : 这里不需要传参了 !
ad.out(outUser, money);
// 模拟转账过程中的异常
// int i = 1 / 0;
// 转入
ad.in(inUser, money);
//事务提交
JdbcUtils.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose();
return false;
}
return true;
}
}

2.2、Dao层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

public void out(String outUser, int money) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//照常使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}

public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}

2.3、Utils方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
//ThreadLocal对象 : 将connection绑定在当前线程中
private static final ThreadLocal<Connection> tl = new ThreadLocal();

// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();

// 获取连接
public static Connection getConnection() throws SQLException {
//取出当前线程绑定的connection对象
Connection conn = tl.get();
if (conn == null) {
//如果没有,则从连接池中取出
conn = ds.getConnection();
//再将connection对象绑定到当前线程中
tl.set(conn);
}
return conn;
}

//释放资源
public static void release(AutoCloseable... ios) {
for (AutoCloseable io : ios) {
if (io != null) {
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

public static void commitAndClose() {
try {
Connection conn = getConnection();
//提交事务
conn.commit();
//解除绑定
tl.remove();
//释放连接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

public static void rollbackAndClose() {
try {
Connection conn = getConnection();
//回滚事务
conn.rollback();
//解除绑定
tl.remove();
//释放连接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

可以看到,在Utils方法中,getConnection的时候,此时使用了一个ThreadLocal对象,将当前Connection对象和当前线程进行绑定了;如果是第一次获取connection对象,那么就从连接池中获取,不是的话,那么直接从ThreadLocal中获取。

3、内部结构探索

3.1、内部结构

在JDK8中ThreadLocal的设计:每个Thread维护一个ThreadLocalMap,这个Map的keyThreadLocal对象本身,而value就是真正需要存储的值。

具体:

(1) 每个Thread线程内部都有一个Map (ThreadLocalMap)
(2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

image-20230722171217698

由于每个Thread中维护一个ThreadLocalMap,Map的key为ThreadLocal对象本身,value为设置的值,这样的优势:

  1. 每个Map存储的Entry数量就会变少,JDK7中的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。(ThreadLocal的数量远远小于Thread数量)
  2. Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

3.2、核心方法

3.2.1、set

1
2
3
4
5
6
7
8
9
10
11
12
13
public void set(T value) {
// 1)拿到当前线程
Thread t = Thread.currentThread();
// 2)通过线程内部的 threadLocals 变量,拿到对应 ThreadLocalMap 对象。对应着分析1
ThreadLocalMap map = getMap(t);
// 3)判断如果不为 null ,则直接调用 ThreadLocalMap 中的 set 方法,传入 当前的 ThreadLocal 对象和要指定修改的值 value,对应着分析2
if (map != null)
map.set(this, value);
else
// 4)创建 map 为 null,就创建 map, 对应着分析3
createMap(t, value);
}


1
2
3
4
5
6
7
8
9
10
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

// Thread类中持有一个ThreadLocalMap类型的对象threadLocals
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}


真正进行赋值:

set方法可以进行修改或者新建的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void set(ThreadLocal<?> key, Object value) {
// 将ThreadLocal对象的存储表table赋值给局部变量tab
Entry[] tab = table;
// 计算tab的长度
int len = tab.length;
// 先找到对应Entry的数组下标
int i = key.threadLocalHashCode & (len-1);
// 循环查找存储表中能匹配的Entry对象,从索引位置开始一直到链表末尾
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 取出当前的ThreadLocal对象
ThreadLocal<?> k = e.get();
// 判断与Key,即是否是一个ThreadLocal对象,如果是,那么就进行以及修改
if (k == key) {
e.value = value;
return;
}
// 当前Entry的ThreadLocal对象为空,说明该Entry无效,可能在之前被GC掉了
if (k == null) {
// 传入key, value, i创建一个新的Entry,存储在数组tab的位置
replaceStaleEntry(key, value, i);
return;
}
}

// 循环找都没有匹配ThreadLocal对象
// 新建一个Entry,
tab[i] = new Entry(key, value);
// 增加存储表中的Entry数量
int sz = ++size;
// 判断是否需要清理一些无效的Entry&&是否需要去扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 进行扩容
rehash();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个默认长度大小为 16 的 Entry 数组
table = new Entry[INITIAL_CAPACITY];
// 计算对应的数组的下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 插入节点
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}

3.2.2、get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public T get() {
// 获取当前的线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通过 getEntry 找到线程对应着的 Entry 对象, 对应着分析1
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为 null 则直接拿到返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map为空,进行initialValue
return setInitialValue();
}


1
2
3
4
5
6
7
8
9
10
11
12
private Entry getEntry(ThreadLocal<?> key) {
// 计算出 index 的值
int i = key.threadLocalHashCode & (table.length - 1);
// 获取当前tab下表为i的Entry
Entry e = table[i];
// 如果存在, 判断是不是相同的对象,是就直接返回
if (e != null && e.get() == key)
return e;
else
// 清空 key 为 null 的对象
return getEntryAfterMiss(key, i, e);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private T setInitialValue() {
// 会进行初始化, 如果我们重写了就会调用我们自己重写的,否则就调用默认的。
// protected T initialValue() {return null;}
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果 map 不为 null ,就直接添加本地变量,key 为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
else
// 如果 map 为 null,说明首次添加,需要首先创建出对应的 map
createMap(t, value);
return value;
}

3.2.3、remove

1
2
3
4
5
6
7
8
public void remove() {
// 获取当前线程绑定的 threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果 map 不为 null,就移除当前线程中指定 ThreadLocal 实例的本地变量
if (m != null)
m.remove(this);
}

4、ThreadLocal内存泄漏

4.1、内存泄漏是什么?

不再会使用的对象或者变量占用的内存不能被回收,就是内存泄漏。

4.2、四种引用

4.2.1、强引用

一般我们 new 关键字创建的对象就是 Reference(强引用),当内存不足时,JVM 开始垃圾回收,对于强引用对象,就算是出现 OOM 也不会对该对象进行回收。

4.2.2、软引用

软引用是一种相对相对于强引用弱化了一些的引用,需要用 SoftReference 类实现,对于软引用来说,当系统内存充足时,软引用对象不会被垃圾回收,不充足时,会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

4.2.3、弱引用

弱引用需要用 WeakReference 类实现,它比软引用的生存期更短,对于弱引用对象来说,只要垃圾回收器运行,不管 JVM 内存空间是否足够,都会回收该对象占用的内存。

4.2.4、虚引用

虚引用需要 PhantomReference 类来实现,如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能会垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。

4.3、Entry

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}
}

从上述代码中可以得知,

  • ThreadLocalMap是ThreadLocal的一个内部静态类,用来存储每个线程对应的变量值。Entry类来管理每个线程本地变量的key-value。
  • EntryThreadLocalMap的一个内部静态类,继承自WeakReference<ThreadLocal<?>>Entry表示一个键值对,用于将ThreadLocal对象与其对应的变量值关联起来。
    • 表明:ThreadLocal对象在没有其他强引用对象的时候会被垃圾回收器进行回收,而Entry的声明周期也会随着结束,进而避免了内存泄漏。

4.3.1、为什么是弱引用

图一

代码演示:

1
2
3
4
5
public void method() {
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl .set(2021);
tl .get();
}
  • 当调用method的时候,会向栈中插入一条栈帧。
  • new关键字创建一个ThreadLocal对象,此时tl是对象的引用
    • new出的对象是一个强引用,通过set方法进行存储值,Key是ThreadLocal对象本身,Value为需要存储的值。
    • Entry继承WeakReference,那么Key是弱引用指向了ThreadLocal对象。
  • 当method方法执行完毕之后,栈帧销毁,此时强引用tl就不存在了。
    • 但是Thread的ThreadLocalMap中的某一个Entry的key的引用还指向了ThreadLocal对象
    • 如果这个Key引用是强引用,会导致Key指向的ThreadLocal对象是强引用对象不能被GC,会造成内存泄漏
    • 如果这个Key引用是弱引用,会大概率减少内存泄漏的问题。使用了弱引用,就可以使ThreadLocal对象在方法执行完毕之后顺利被回收,并且Key的引用会被指向为null。

图二

总结:

  • new一个ThreadLocal对象的时候,就会有一个强引用指向这个对象。
  • 调用set方法之后,线程中的ThreadLocalMap中的Entry对象中的Key指向ThreadLocal对象。
  • 如果Key是强引用的话,当方法执行完,栈帧中的强引用销毁了,对象还不能被回收,此时就会造成内存泄漏。

4.3.2、为什么还是会泄漏

虽然Entry继承了弱引用,保证了Key指向的ThreadLocal对象能被及时回收,但是此时v指向的Value对象需要再ThreadLocalMap调用get、set的时候发现Key为null的时候才能回收整个的entry、Value。

为什么value还持有引用?

解答:ThreadLocal作为Thread的一个属性,如果当前线程没有手动销毁,那么ThreadLocalMap也还是存在,同理Entry的引用也持有。

所以泄露的根本原因就是因为ThreadLocal的生命周期和Thread的生命周期一样,如果线程没有主动销毁,那么entry就不会被销毁。
image-20230725174415384

所以:弱引用只是帮助我们降低了内存泄漏的概率,并不能完全避免,在使用完成之后,必须手动remove这个对象。

在这里插入图片描述


Java多线程-ThreadLocal
https://baijianglai.cn/Java多线程-ThreadLocal/5c8cef3482ea/
作者
Lai Baijiang
发布于
2023年7月22日
许可协议