关于ThreadLocal
ThreadLock(线程本地存储)
在之前学习关于线程安全的时候,知道了解决线程不安全问题的三种方式,即:
- synchronized同步锁 (加锁和释放锁);
- Lock 手动锁;
- 使用私有变量(ThreadLocal );
在这里将对第三种方式:使用私有变量(ThreadLocal )进行进一步学习。
ThreadLocal(线程级别的私有变量),很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
例题:
线程池:10 任务数:1000
任务级别的私有变量:创建1000个私有变量。
ThreadLocal私有变量:创建10个私有变量。
注意:
加锁可以解决线程不安全问题,但同时都带来了新的问题,即排队执行会消耗性能。而ThreadLocal虽然避免了排队执行但每次都要创建对象,因而也消耗了时间。
ThreadLocal 的使用:
1. set():将私有变量存放到线程中。
2. get():从线程中取得私有变量。
3. remove():从线程中移除私有变量(脏读,内存溢出)。
public class ThreadDemo67 {
// 创建 ThreadLocal
static ThreadLocal<String> threadLocal =
new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// set threadLocal
String tname = Thread.currentThread().getName();
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "线程1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
String tname = Thread.currentThread().getName();
// set ThreadLocal
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "线程2");
t2.start();
}
private static void printTName() {
String tname = Thread.currentThread().getName();
// 得到存放在 ThreadLoca 中的值
String result = threadLocal.get();
System.out.println(String.format("线程:%s,取得:%s",
tname, result));
}
4.initialValue():初始化ThreadLocal
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadDemo68 {
// 创建并初始化
static ThreadLocal<String> threadLocal =
new ThreadLocal() {
@Override
protected String initialValue() {
System.out.println("执行了初始化方法");
return "Java";
}
};
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
for (int i = 0; i < 2; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("得到数据:" + result);
}
});
}
}
}
注意:
initialValue() + get ()正常存取操作。
initialValue() + set() + get() ?
A.先执行初始化方法、再执行set方法、再执行get。
B.先执行initialValue、再执行get 。
C.先执行set方法、再执行get方法,不执行initialValue()。(正确)
面试题: 什么情况下不会执行initialValue()? 为什么?
set 之后就不会执行了。
ThreadLocal是懒加载的,当调用了get方法之后,才会尝试执行 initialValue(初始化)方法。
在执行initialValue之前会尝试获取一下ThreadLocal set 的值,如果获取到了值,那么初始化方法永远不会执行
ThreadLocalMap(线程的一个属性)
- 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
- 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
- ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
5.withInitial():初始化ThreadLocal
支持lambda表达式。
public class ThreadDemo71 {
static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(() -> "Java");
public static void main(String[] args) {
try {
String result = threadLocal.get();
System.out.println("结果:" + result);
} finally {
// remove 在任何场景下都有意义
threadLocal.remove();
}
try {
String result2 = threadLocal.get();
System.out.println("结果2:" + result2); // JAVA
} finally {
threadLocal.remove();
}
}
}
ThreadLock 使用场景:
- 解决线程安全问题;
- 实现线程级别的数据传递;
ThreadLocal的缺点:
1. 不可继承性
子线程中不能读取到父线程的值。
/**
* 缺点 1:不可继承性
*/
public class ThreadLocal75 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// set ThrealLocal
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// get ThreadLocal
String result = threadLocal.get();
System.out.println("结果:" + result);
}
});
t1.start();
}
}
解决方法:
在new时,不再new ThreadLocal<>(); 而是 new InheritableThreadLocal<>();
/**
* 可继承性
*/
public class ThreadLocal76 {
static ThreadLocal<String> threadLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
// set ThrealLocal
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// get ThreadLocal
String result = threadLocal.get();
System.out.println("结果:" + result);
}
});
t1.start();
}
}
注意事项: InheritableThreadLocal不能实现不同线程之间的数据共享。
public class ThreadLock77 {
static ThreadLocal<String> threadLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// set ThrealLocal
threadLocal.set("Java");
System.out.println("线程 1 设置了值:Java");
}
});
t1.start();
// 等待执行完
t1.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// get ThreadLocal
String result = threadLocal.get();
System.out.println("线程 2 得到结果:" + result);
}
});
t2.start();
}
}
2.脏读(脏数据):
在一个线程中读取到了不属于自己的数据。
**线程使用ThreadLocal不会出现脏读,**因为每个线程都使用的是自己的变量值和ThreadLocal。
线程池里面使用ThreadLocal就会出现脏数据,线程池会复用线程,复用线程之后,也会复用线程中的静态属性,从而导致了某些方法不能被执行,于是就出现了脏数据的问题。
解决方案:
- 避免使用静态属性(静态属性在线程池中会复用);
- 使用remove()方法;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLock79 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 2; i++) {
MyTask t = new MyTask();
executor.execute(t);
}
}
static class MyTask extends Thread {
// 标识是否第一次访问
static boolean first = true;
@Override
public void run() {
if (first) {
// 第一次访问,存储用户信息
threadLocal.set(this.getName() +
" session info.");
first = false;
}
try {
// get threadLocal
String result = threadLocal.get();
System.out.println(this.getName() +
":" + result);
} finally {
threadLocal.remove();
}
}
}
}
3.内存溢出问题(最常出现):
(线程不会出现)
public class ThreadLocal81 {
// 1m 大小的对象
static class OOMObject {
private byte[] bytes = new byte[1 * 1024 * 1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
int finalI = i;
Thread t = new Thread(new Runnable() {
@Override
public void run() {
OOMObject oomObject = new OOMObject();
// set threadLocal
System.out.println("任务:" + finalI + " 执行了");
threadLocal.set(oomObject);
// 不用对象了
oomObject = null;
}
});
t.start();
Thread.sleep(200);
}
}
}
内存溢出: 当一个线程执行完之后,不会释放这个线程所占用内存,或者释放内存不及时的情况都叫做内存溢出。( 线程不用了,但线程相关的内存没得到及时的释放)
内存溢出: ThreadLocal +线程池
线程池是长生命周期,而线程是执行完任务就结束了(线程相关的资源都会释放掉)。
ThreadPool(创建后不会销毁) -> Thread -> ThreadLocal ->内存不会关闭-> 00M
查看ThreadLocal的set()源码如下:
此处引出一个面试题:
面试题:1.Hashmap和ThreadLocalMap处理hash冲突的区别?
答: hashmap使用的链表法,而ThreadLocalMap使用的是开放寻址法。
2.为什么要这样实现?
答:开放寻址法它的特点和使用场景是数据量比较少的情况下性能更好;而HashMap里面存储的数据通常情况下是比较多,这个时候使用开放寻址法效率就比较低了,这个时候最好使用链式法。
以下代码模拟内存溢出情况,需设置JVM内存。
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocal82 {
// 1m 大小的对象
static class OOMObject {
private byte[] bytes = new byte[1 * 1024 * 1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
OOMObject oomObject = new OOMObject();
// set threadLocal
System.out.println("任务:" + finalI + " 执行了");
threadLocal.set(oomObject);
// 不用对象了
oomObject = null;
}
});
Thread.sleep(200);
}
}
}
解决方法:
使用remove()
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocal83 {
// 1m 大小的对象
static class OOMObject {
private byte[] bytes = new byte[1 * 1024 * 1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
OOMObject oomObject = new OOMObject();
// set threadLocal
System.out.println("任务:" + finalI + " 执行了");
threadLocal.set(oomObject);
// 不用对象了
oomObject = null;
} finally {
// 解决内存溢出问题
threadLocal.remove();
}
}
});
Thread.sleep(200);
}
}
}
补充:
面试题: 将ThreadLocal 中的key设置为弱引用?
答:为了最大K度的避免00M。
关于一切ThreadLocal的问题都可用remove()解决。