存在的以下代码所示的线程隔离问题:
package study.用ThreadLocal解决线程隔离问题;
/*
线程隔离 - 在多线程并发场景下,每个线程的变量都应该是相互独立的
线程A:设置(变量1) 获取(变量1)
线程B:设置(变量2) 获取(变量2)
*/
public class 存在的线程隔离问题 {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
存在的线程隔离问题 demo = new 存在的线程隔离问题();
// 多个线程同时访问和修改同一个 存在的线程隔离问题 类对象 demo 的属性 content,这会导致线程之间的竞争问题。
// 由于 content 变量是共享的,并且没有任何同步控制,所以多个线程可能会相互覆盖彼此的数据,从而导致不可预测的结果。
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
/*
每个线程:存一个变量,过一会再取出这个变量
*/
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("---------------------------");
System.out.println(Thread.currentThread().getName() + "------>" + demo.getContent());
}
});
thread.setName("线程" + i); // 给每个线程设置线程名
thread.start();
}
}
}
结果:
使用ThreadLocal解决上面线程隔离问题。
package study.用ThreadLocal解决线程隔离问题;
/*
线程隔离 - 在多线程并发场景下,每个线程的变量都应该是相互独立的
线程A:设置(变量1) 获取(变量1)
线程B:设置(变量2) 获取(变量2)
ThreadLocal:
1. set() : 将变量绑定到当前线程中
2. get() : 获取当前线程绑定的变量
*/
public class 存在的线程隔离问题 {
ThreadLocal<String> tl = new ThreadLocal<>();
private String content;
private String getContent() {
return tl.get();
}
private void setContent(String content) {
tl.set(content);
// this.content = content;
}
public static void main(String[] args) {
存在的线程隔离问题 demo = new 存在的线程隔离问题();
// 多个线程同时访问和修改同一个 存在的线程隔离问题 类对象 demo 的属性 content,这会导致线程之间的竞争问题。
// 由于 content 变量是共享的,并且没有任何同步控制,所以多个线程可能会相互覆盖彼此的数据,从而导致不可预测的结果。
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
/*
每个线程:存一个变量,过一会再取出这个变量
*/
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("---------------------------");
System.out.println(Thread.currentThread().getName() + "------>" + demo.getContent());
}
});
thread.setName("线程" + i); // 给每个线程设置线程名
thread.start();
}
}
}
结果:
如果使用synchronized
也可以完成一样的效果,但是却牺牲了程序的并发性。
synchronized(存在的线程隔离问题.class) {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("---------------------------");
System.out.println(Thread.currentThread().getName() + "------>" + demo.getContent());
}
另一个需求场景:
事务的使用注意点:
- service层和dao层的连接对象保持一致
- 每个线程的connection对象必须前后一致,并且不同线程之间线程隔离,你处理你的,我处理我的,互不干扰。
常规的解决方案:
- 传参:将service层的connection对象直接传递到dao层
- 加锁
弊端
- 将 Connection 对象直接从 service 层传递到 DAO 层确实会提高耦合度,这是因为这种做法违反了分层架构的原则,使各层之间的依赖关系更加紧密。耦合度(Coupling)是指两个模块或类之间互相依赖的程度。高耦合意味着模块之间的依赖关系较强,任何一个模块的变化都会影响到其他模块。低耦合则意味着模块之间的依赖关系较弱,模块可以独立地进行修改和维护。
- 因为存在加锁的同步,使得在多线程环境下降低并发度从而降低程序性能。
使用ThreadLocal相比常规的解决方案的优势
- 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离又兼具并发性,避免同步方式带来的性能损失
内部结构
- 早期的ThreadLocal设计是ThreadLocal来维护ThreadLocalMap,每个Thread类线程对象作为Map的Key
- 而JDK8的设计是让每一个Thread类线程对象来维护ThreadLocalMap,当前线程的每一个ThreadLocal作为Map的Key
好处:
- 实际开发中,ThreadLocal的数量往往少于线程Thread的数量,所以对于JDK8这种设计来说,每个Map存储的Entry的数量就会变少,这样就可以尽量避免哈希冲突的发生
- 当Thread销毁的时候,ThreadLocal也会随之销毁,从而能够及时回收内存
核心方法
set方法
- 首先获取当前线程,并根据当前线程获取它的Map
- 如果获取的Map不为空,则将此参数设置到Map中(当前ThreadLocal的引用作为Key)
- 如果Map为空,则给该线程创建Map,并设置初始值
/ *
设置当前线程对应的ThreadLocal的值
value是将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null) {
// 存在则调用map.set设置此实体entry
// 当前的ThreadLocal作为Key
// 当前线程需要绑定的值作为Value
map.set(this, value);
} else {
// 1) 当前线程Thread不存在ThreadLocalMap对象
// 2) 则调用createMap进行ThreadLocalMap对象初始化
// 3) 并将t(当前线程)和value(t对应的值)作为第一个entry存放进ThreadLocalMap中
createMap(t, value);
}
}
/*
获取当前线程Thread对应维护的ThreadLocalMap
t就是当前线程
返回对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread.java源码中,关于threadLocals的描述如下
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法
- 首先获取当前线程,根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来从Map中获取对应的Enrty e,否则转到4.
- 如果e不为null,则返回e.value,否则转到4.
- Map为空或者e为空,则通过initialValue函数获取初始值value(子类不重写的话,默认就是null了),然后用ThreadLocal的引用和初始值value作为新建Map的第一个key和value。
/*
返回当前线程中保存的ThreadLocal的值
如果当前线程没有此ThreadLocal变量,则它会通过initialValue方法进行初始化
返回当前线程对应的ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 因为map的Key是用ThreadLocal作为Key
// 所以以当前的ThreadLocal为Key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体e对应的value值
// 即为我们想要的当前线程对应ThreadLocal绑定的那个值
T result = (T)e.value;
return result;
}
}
/*
会进行如下初始化的两种情况
第一种情况:map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况:map存在,但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
/*
初始化
返回初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化后的值
// 此方法可以被子类重写,如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
remove方法
- 首先获取当前线程,并根据当前线程获取一个属于那个线程的Map
- 如果获取的Map不为空,则移除当前ThreadLocal对象对应的键值对entry,如果Map中不存在当前ThreadLocal对象对应的键值对entry,则不用管
/*
删除当前线程中保存的ThreadLocal对应的实体entry(也就是键值对)
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null) {
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
}
存在的内存泄漏问题
Memory overflow:内存溢出,没有足够的内存提供申请者使用
Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。