ThreadLocal 的简单方法:
- set() 将私有变量设置到线程中(也就是说每个线程有自己的变量)
- get() 从线程中取私有变量的值
- remove() 从线程中移除私有变量
public class ThreadLocalDemo68 {
//创建线程的私有变量
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//定义公共的任务
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//得到线程名称
String tname = Thread.currentThread().getName();
System.out.println(tname +"设置值:"+tname);
// 设置ThreadLocal
threadLocal.set(tname);
//执行打印
printThreadLocal();
} finally {
//进行remove操作
threadLocal.remove();
}
}
};
Thread thread = new Thread(runnable,"线程1");
thread.start();
Thread thread1 = new Thread(runnable,"线程2");
thread1.start();
}
private static void printThreadLocal() {
//从Thread Local中获取值
String result = threadLocal.get();
System.out.println(Thread.currentThread().getName()+"取值:"+result);
}
}
线程1设置值:线程1
线程2设置值:线程2
线程1取值:线程1
线程2取值:线程2
ThreadLocal的高级方法:
- 用 initialValue 进行初始化
- withInitial 静态方法
用 initialValue 进行初始化的用法:
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalDemo69 {
//创建ThreadLocal的时候设置默认值
//创建并设置默认值
private static ThreadLocal<SimpleDateFormat> threadLocal =
new ThreadLocal(){
@Override
//注意返回值的类型要和上面定义的创建的ThreadLocal的类型相同
protected SimpleDateFormat initialValue() {
System.out.println("执行 initialValue 方法");
return new SimpleDateFormat("mm:ss");
}
};
public static void main(String[] args) {
//两个线程来演示
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date(1000);
String s = threadLocal.get().format(date);
System.out.println("线程1 时间格式化: "+ s);
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date(2000);
String s = threadLocal.get().format(date);
System.out.println("线程2 时间格式化: "+ s);
}
});
t2.start();
}
}
执行 initialValue 方法
执行 initialValue 方法
线程1 时间格式化: 00:01
线程2 时间格式化: 00:02
import java.util.Random;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo70 {
private static ThreadLocal<Integer> threadLocal =
new ThreadLocal() {
@Override
protected Integer initialValue() {
int num = new Random().nextInt(10);
System.out.println("执行了 initialValue 方法 生成了: "+num);
return num;
}
};
public static void main(String[] args) {
//一个线程池来进行演示
//线程池里面只有一个线程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
int num = threadLocal.get();
System.out.println(Thread.currentThread().getName()+"得到值: "+num);
}
});
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
int num = threadLocal.get();
System.out.println(Thread.currentThread().getName()+"得到值: "+num);
}
});
}
}
执行了 initialValue 方法 生成了: 2
pool-1-thread-1得到值: 2
pool-1-thread-1得到值: 2
可以发现,在线程池里面使用ThreadLocal方法的时候,当设置线程池里面线程的个数是1的时候,initialValue 方法只执行了一次,并且两取到的值相同,说明initialValue 是线程级别的方式,所以initialValue 的方法才只会执行一次。
withInitial 静态方法
import java.util.function.Supplier;
public class ThreadLocalDemo71 {
//静态的高级的用法 withInitial
//创建并初始化ThreadLocal
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了 withInitial 方法");
return Thread.currentThread().getName()+" Java";
}
});
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("获取到的内容:"+result);
}
};
Thread t1 = new Thread(runnable,"线程1");
t1.start();
Thread t2 = new Thread(runnable,"线程2");
t2.start();
}
}
执行了 withInitial 方法
执行了 withInitial 方法
获取到的内容:线程1 Java
获取到的内容:线程2 Java
上面的静态的 withInitial 和 initialValue 的作用看起来没有什么区别,那么为什么还要有 withInitial 方法呢?
withInitial 在JDK1.8以后可以使用简单的初始化方式,如下:
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo72 {
//withInitial 简单初始化版本
//使用lamda表达式
//创建并初始化ThreadLocal
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(()->"Java");
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("得到结果 "+result);
}
});
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("得到结果 "+result);
}
});
}
}
得到结果 Java
得到结果 Java
面试题:ThreadLocal 的初始化方法在什么情况下不执行
答:当ThreadLocal中出现set方法之后,所有类型的初始化方法就不会执行了。
原因:ThreadLocal在执行get方法的时候,才会去判断并调用初始化方法
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;
}
}
return setInitialValue();
}
下面举个例子来说明一下:
public class ThreadLocalDemo73 {
//创建并初始化ThreadLocal
private static ThreadLocal<String> threadLocal =
new ThreadLocal() {
@Override
protected String initialValue() {
System.out.println("执行了 initialValue 方法");
return Thread.currentThread().getName()+" Java";
}
};
/*ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了 withInitial 方法");
return Thread.currentThread().getName()+" Java";
}
});*/
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
/**
* 当ThreadLocal中既有set方法,又有初始化方法的时候,所有的初始化方法都不会执行!!!!
*/
threadLocal.set(Thread.currentThread().getName()+" 你好");
String result = threadLocal.get();
System.out.println("线程1 获取到的内容:"+result);
}
};
Thread t1 = new Thread(runnable,"线程1");
t1.start();
Thread t2 = new Thread(runnable,"线程2");
t2.start();
}
}
线程1 获取到的内容:线程2 你好
线程1 获取到的内容:线程1 你好
面试题:ThreadLocal只能存储一个值,但是为什么底层还要用ThreadLocalMap来实现存值取值??
因为ThreadLocalMap里面存储的是所有的线程的值,而不是一个线程的值。
ThreadLocal还是锁?
就看创建实例对象之后的复用率,复用率高就是ThreadLocal
ThreadLocal 经典使用场景:
- 1.解决线程不安全的问题
- 2.线程级别的数据传递(可以实现一定程度的解耦)
ThreadLocal缺陷:
- 1.不可继承性(子线程不能继承主线程的私有变量)
- 2.脏数据(一个程序的任务执行当中,读取了其他任务的数据)
- 3.内存溢出
不可继承性演示:
public class ThreadLocalDemo76 {
static ThreadLocal threadLocal = new ThreadLocal();
public static void main(String[] args) {
//在主线程里面设置值
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("获取值:"+threadLocal.get());
}
});
t1.start();
}
}
在子线程里面获取不到主线程的值
获取值:null
解决不可继承性的问题的方法: InheritableThreadLocal()
public class ThreadLocalDemo77 {
//创建可以继承的ThreadLocal
private static ThreadLocal threadLocal = new InheritableThreadLocal();
public static void main(String[] args) {
//在主线程里面设置值
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("获取值:"+threadLocal.get());
}
});
t1.start();
}
}
获取值:Java
下面的代码不能实现在线程2里面得到线程1设置的值,因为线程1和线程2不是父子类关系,而上面的main函数和t1线程是父子类关系,所以使用 InheritableThreadLocal 实现的 ThreadLocal 可以得到父类的值。
public class ThreadLocalDemo78 {
//创建可以继承的ThreadLocal
private static ThreadLocal threadLocal = new InheritableThreadLocal();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("Java");
System.out.println("线程1设置值");
}
});
t1.start();
t1.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2获取值:"+threadLocal.get());
}
});
t2.start();
t2.join();
}
}
线程1设置值
线程2获取值:null
注意事项:即使使用了 InheritableThreadLocal 也不能实现并列线程之间的数据传递(数据设置和获取)
ThreadLocal 脏读:在一个线程中读取到了不属于自己的信息。(两个任务共用一个线程的时候会发生脏读现象)
/**
* 未发生脏读的演示代码
*/
public class ThreadLocalDemo79 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
MyThreadLocal myThreadLocal = new MyThreadLocal();
myThreadLocal.show();
}
},"线程1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
MyThreadLocal myThreadLocal = new MyThreadLocal();
myThreadLocal.show();
}
},"线程2");
t2.start();
}
static class MyThreadLocal{
private static boolean flag = false;
public void show() {
String name = Thread.currentThread().getName();
if (!flag) {
//第一次执行
threadLocal.set(name);
System.out.println(name +" 设置了: "+name);
flag = true;
}
System.out.println(name+" 得到了:"+threadLocal.get());
}
}
}
线程2 设置了: 线程2
线程1 设置了: 线程1
线程2 得到了:线程2
线程1 得到了:线程1
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 发生脏读的演示
* 线程池演示
*/
public class ThreadLocalDemo80 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
for (int i = 0; i < 2; i++) {
executor.submit(new MyThreadLocal());
}
}
static class MyThreadLocal extends Thread{
private static boolean flag = false;
@Override
public void run() {
String name = this.getName();
//String name = Thread.currentThread().getName();
if (!flag) {
//第一次执行
threadLocal.set(name);
System.out.println(name+" 设置了: "+name);
flag = true;
}
System.out.println(name +"得到了:"+threadLocal.get());
}
}
}
Thread-0 设置了: Thread-0
Thread-0得到了:Thread-0
Thread-1得到了:Thread-0
问题出现了,只设置了一次,但是得到了两次结果
脏读产生的原因:线程复用线程,和线程相关的静态属性也进行复用了,所以就导致了脏读。
如何避免脏数据:
- 1.避免使用静态变量
- 2.使用完之后,进行remove操作(可以解决所有的问题)
内存溢出:当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就会造成内存溢出。( 前置条件:配合使用线程池才会出现内存溢出)
下面进行代码演示:
设置最大的运行内存,设置5M大小的运行内存
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo81 {
private static ThreadLocal<MyThreadLocal> threadLocal = new ThreadLocal<>();
/**
* 使用线程池的方式来进行演示
*/
//创建大对象
static class MyThreadLocal{
private byte[] bytes = new byte[1024*1024]; //1M大小
}
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
for (int i = 0; i < 5; i++) {
//循环5次就足以检测到内存溢出,因为每一次创建的时候还有许多额外的内存占用的程序
executor.excute(new Runnable() {
@Override
public void run() {
//在ThreadLocal里面每一次设置1M大小的值
threadLocal.set(new MyThreadLocal());
System.out.println(Thread.currentThread().getName()+"线程名设置了值。");
}
});
}
}
}
pool-1-thread-4线程名设置了值。
pool-1-thread-5线程名设置了值。
pool-1-thread-1线程名设置了值。
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-2" java.lang.OutOfMemoryError: Java heap space
at thread.thread0522.ThreadLocalDemo81$MyThreadLocal.<init>(ThreadLocalDemo81.java:15)
at thread.thread0522.ThreadLocalDemo81$1.run(ThreadLocalDemo81.java:29)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.OutOfMemoryError: Java heap space
at thread.thread0522.ThreadLocalDemo81$MyThreadLocal.<init>(ThreadLocalDemo81.java:15)
at thread.thread0522.ThreadLocalDemo81$1.run(ThreadLocalDemo81.java:29)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
内存溢出的原因:
- 1.因为线程池是长生命周期的(使用结束以后不会释放内存)
- 2.ThreadPoll -> Thread -> ThreadLocal -> ThreadLocalMap -> Entry[ ] key,value(在下面的例子里面 每个value是1mb资源) ,垃圾回收器就不会回收value资源(因为value是强引用)
Java引用的4种类型:
- 1.强引用: Object obj = new Object(); (使用 = 进行初始化) 即使发生OOM也不会进行垃圾回收。
- 2.软引用:它的应用关系仅次于强引用,如果内存够用,那么垃圾回收器不会考虑回收此引用,将要发生OOM的时候才会回收此引用
- 3.弱引用:不管内存是否够用,下一次回收的时候,都会将此引用的对象回收掉
- 4.虚引用:创建即回收 存在的意义:可以触发一个垃圾回收的回调函数。
面试题:为什么ThreadLocal 会将key设置为弱引用?
答:ThreadLocal 为了更大程度的避免OOM。
如何解决内存溢出?
答:使用完成之后remove()
代码演示如下:
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo83 {
private static ThreadLocal<MyThreadLocal> threadLocal = new ThreadLocal<>();
//创建大对象
static class MyThreadLocal{
private byte[] bytes = new byte[1024*1024]; //1M大小
}
public static void main(String[] args) throws InterruptedException {
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10));
for (int i = 0; i < 5; i++) {
Thread.sleep(300); //设置任务的时候慢一点,否则就算用remove进行移除也还是会报错
//循环5次就可以检测到内存溢出,因为每一次创建的时候还有许多额外的内存占用的程序
executor.execute(new Runnable() {
@Override
public void run() {
try {
//在ThreadLocal里面每一次设置1M大小的值
threadLocal.set(new MyThreadLocal());
System.out.println(Thread.currentThread().getName()+"线程名设置了值。");
} finally {
//移除变量,防止OOM
threadLocal.remove();
}
}
});
}
}
}
哈希冲突的解决方案不同:
-
HashMap:链表法(JDK8以后是 链表+红黑树)
链表升级为红黑树的条件:1.链表的长度大于8 2.数组的长度大于64 红黑树降级为链表:链表的长度小于6之后 从链表升级为红黑树的原因:加速查询性能
-
ThreadLocalMap:开放寻址法(开放定址法,从冲突的地方开始向后寻找)(数据量小的使用场景)