ThreadLocal
基本原理
ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。
- 在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。
- ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法。
基本思路: 当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
- ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示
API 事例
- ①.
protected T initialValue():initialValue():
返回此线程局部变量的当前线程的"初始值"
(对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法) - ②.
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
: 创建线程局部变量 - ③.
T get()
:返回当前线程的此线程局部变量的副本中的值 - ④.
void set(T value)
: 将当前线程的此线程局部变量的副本设置为指定的值 - ⑤.
void remove()
: 删除此线程局部变量的当前线程的值
例子:
/***
* 看每个销售员可以出售多少套房子
*/
class House{
/**
initialValue():返回此线程局部变量的当前线程的"初始值"
对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法
ThreadLocal<Integer>threadLocal=new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};*/
//public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
//withInitial(Supplier<? extends S> supplier):创建线程局部变量
//ThreadLocal本地线程变量,线程自带的变量副本
ThreadLocal<Integer>threadLocal=
ThreadLocal.withInitial(()->0);
public void saleHouse(){
//T get():返回当前线程的此线程局部变量的副本中的值。
Integer value = threadLocal.get();
value++;
//void set(T value):将当前线程的此线程局部变量的副本设置为指定的值。
threadLocal.set(value);
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
new Thread(()->{
try{
for (int i = 1; i <=3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
//void remove():删除此线程局部变量的当前线程的值
//在阿里巴巴手册中有说明,尽量在代理中使用try-finally块进行回收
house.threadLocal.remove();
//下面获取到的值是线程的初始值0
System.out.println("**********"+house.threadLocal.get());
}
},"t1").start();
new Thread(()->{
try{
for (int i = 1; i <=5; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
house.threadLocal.remove();
}
},"t2").start();
new Thread(()->{
try{
for (int i = 1; i <=8; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
house.threadLocal.remove();
}
},"t3").start();
System.out.println(Thread.currentThread().getName()+"\t"+"卖出了:"+house.threadLocal.get());
}
}
/**
* t1 卖出:3
* t2 卖出:5
* **********0
* main 卖出了:0
* t3 卖出:8
* */
- 因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用。既然其他Thread不可访问,那就不存在多线程共享的问题
- 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
- 加入 synchronized 或者 lock 控制线程的访问顺序,而 ThreadLocal 人手一份,大家各自安好,没必要抢夺
SimpleDateFormat 的不安全性
非线程安全的SimpleDateFormat
写时间工具类,一般写成静态的成员变量,不知,此种写法的多线程下的危险性!
public class DateUtils
{
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
* @param stringDate
* @return
* @throws Exception
*/
public static Date parseDate(String stringDate)throws Exception
{
return sdf.parse(stringDate);
}
public static void main(String[] args) throws Exception
{
for (int i = 1; i <=30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
上面代码会报错:
原因:
- SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个SimpleDateFormat相关的日期信息
- 例如
sdf.parse(dateStr),sdf.format(date)
诸如此类的方法参数传入的日期相关 String,Date 等等, 都是交由 Calendar引用 来储存的.这样就会导致一个问题:如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用 - 因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。
解决方法:
-
方案一:将SimpleDateFormat定义成局部变量
缺点: 每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收public class DateUtils { public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象 * @param stringDate * @return * @throws Exception */ public static Date parseDate(String stringDate)throws Exception { return sdf.parse(stringDate); } public static void main(String[] args) throws Exception { for (int i = 1; i <=30; i++) { new Thread(() -> { try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.parse("2020-11-11 11:11:11")); sdf = null; } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } }
-
方案二: 加入synchronized,用时间换空间,效率低
/** * 在对一些业务日志写入数据库的时候,日期调用了sdf的静态,导致了会报错或者日期乱了 * */ public class ThreadLocalDataUtils { public static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** 解决方案一:加入synchronized,用时间换空间,效率低 */ /** 如果不加会导致线程安全问题,SimpleDateFormat类内部有一个Calendar对象引用, SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的. 这样就会导致一个问题如果你的SimpleDateFormat是个static的,那么多个thread之间 就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用(相当于买票案列) */ public static synchronized Date parse(String stringDate) throws ParseException { System.out.println(sdf.parse(stringDate)); return sdf.parse(stringDate); } }
-
方案三: 使用ThreadLocal,用空间换时间,效率高
ThreadLocal中变量副本会人手一份,每次使用完了threadLocal后都要将资源进行释放的处理/** * 在对一些业务日志写入数据库的时候,日期调用了sdf的静态,导致了会报错或者日期乱了 * */ public class ThreadLocalDataUtils { public static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static Date parse(String stringDate) throws ParseException { System.out.println(sdf.parse(stringDate)); return sdf.parse(stringDate); } /*** * 解决方案二:使用ThreadLocal,用空间换时间,效率高 * ThreadLocal中变量副本会人手一份,每次使用完了threadLocal后都要将资源进行释放的处理 */ public static final ThreadLocal<SimpleDateFormat>sdfThreadLocal= ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static Date parseByThreadLocal(String stringDate) throws ParseException { return sdfThreadLocal.get().parse(stringDate); } //3 DateTimeFormatter 代替 SimpleDateFormat public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static String formatForDateTime(LocalDateTime localDateTime) { return DATE_TIME_FORMAT.format(localDateTime); } public static LocalDateTime parseForDateTime(String dateString) { return LocalDateTime.parse(dateString,DATE_TIME_FORMAT); } public static void main(String[] args) throws Exception{ for (int i = 1; i <=3; i++) { new Thread(()->{ try { System.out.println(ThreadLocalDataUtils.parseForDateTime("2021-03-30 11:20:30")); } catch (Exception e) { e.printStackTrace(); }finally { ThreadLocalDataUtils.sdfThreadLocal.remove(); } },String.valueOf(i)).start(); } } }
Thread| ThreadLocal|ThreadLocalMap之间的关系
- Thread类中有一个
ThreadLocal.ThreadLocalMap threadLocals = null
的变量,
这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁 - 在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry数组
- 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
t.threadLocals = new ThreadLocalMap(this, firstValue)
:每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本
set方法详解
- ①. 首先获取当前线程,并根据当前线程获取一个Map
- ②. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key) 注意key 是 threadlocal 对象,而不是当前 thread
- ③. 如果Map为空,则给该线程创建 Map,并设置初始值
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/*
* firstKey : 本ThreadLocal实例(this)
* firstValue : 要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引(重点代码)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//设置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
get方法详解
先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的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();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
remove方法详解
- ①. 首先获取当前线程,并根据当前线程获取一个Map
- ②. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry (key为 ThreadLocal对象)
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
为什么源代码用弱引用?
- ①. 当
function1
方法执行完毕后,栈帧销毁 强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象 - ②. 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
- ③. 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷) 。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为nul
弱引用:gc就会被回收
key为null的entry
ThreadLocalMap 使用 ThreadLocal 的弱引用作为key。如果一个ThreadLocal 没有外部强引用引用他,那么系统 gc 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry , 就没有办法访问这些 key 为 null 的Entry 的 value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链
-
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value
-
因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
-
如果当前thread运行结束,threadLocal,threadLocalMap, Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收
-
但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
-
出现内存泄漏的真实原因 (1). 没有手动删除这个Entry (2). CurrentThread依然运行
set、get方法会去检查所有键为null的Entry对象
- set():
- get( ):
- remove( )
- 自定义的 threadlocal 可以在 finally 中进行回收
ThreadLocal总结
- ①. ThreadLocal本地线程变量,以空间换时间,线程自带的变量副本,人手一份,避免了线程安全问题
- ②. 每个线程持有一个只属于自己的专属Map并维护了 Thread Local对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ③. ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- ④. 都会通过expungeStaleEntry,cleanSomeSlots, replace StaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
- ⑤. 用完之后一定要remove操作
中断
概述
- 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,所以,Thread.stop、Thread.suspend、Thread. resume都已经被废弃了
- 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制 — 中断
- 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现
- 若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设为true
- 每个线程对象中都有一个标识,用于标识线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用
void interrupt( )实例方法
interrupt( )
仅仅是设置线程的中断状态未true,不会停止线程
(如果这个线程因为 wait()、join()、sleep() 方法 在用的过程中被打断(interupt),会抛出InterruptedException)
boolean isInterrupted( )实例方法
判断当前线程是否被中断(通过检查中断标识位) 实例方法
static boolean interrupted( )静态方法
判断线程是否被中断,并清楚当前中断状态,这个方法做了两件事
- 返回当前线程的中断状态
- 将当前线程的中断状态设为false
原理: 假设有两个线程A、B,线程B调用了interrupt方法,这个时候我们连接调用两次isInterrupted方法,第一次会返回true,然后这个方法会将中断标识位设置位false,所以第二次调用将返回false
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println("111111");
Thread.currentThread().interrupt();///----false---> true
System.out.println("222222");
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
/**
main---false
main---false
111111
222222
main---true
main---false
* */
比较静态方法interrupted和实例方法isInterrupted
- 静态方法interrupted将会清除中断状态(传入的参数 ClearInterrupted 为 true)
- 实例方法isInterrupted则不会(传入的参数ClearInterrupted 为 false)
如何使用中断标识停止线程
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行型对于的中断处理业务逻辑
三种中断标识停止线程的方式:
-
通过一个volatile变量实现
/** * 通过一个volatile变量实现 */ public static void m1(){ static volatile boolean isStop = false; new Thread(() -> { while(true) { if(isStop) { System.out.println("-----isStop = true,程序结束。"); break; } System.out.println("------hello isStop"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { isStop = true; },"t2").start(); }
-
通过AtomicBoolean
/** * 通过AtomicBoolean */ static AtomicBoolean atomicBoolean = new AtomicBoolean(false); public static void m2(){ new Thread(() -> { while(true) { if(atomicBoolean.get()) { System.out.println("-----atomicBoolean.get() = true,程序结束。"); break; } System.out.println("------hello atomicBoolean"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { atomicBoolean.set(true); },"t2").start(); }
-
通过Thread类自带的中断API方法(isInterruptedk,interrupt )实现
public static void m3(){ Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("-----isInterrupted() = true,程序结束。"); break; } System.out.println("------hello Interrupt"); } }, "t1"); t1.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { t1.interrupt();//修改t1线程的中断标志位为true },"t2").start();
当前线程的中断标识为true,是不是就立刻停止?
线程调用interrupt()时
- 如果线程处于正常活动状态,那么会将线程的中断标志设置位 true ,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以 interrupt( ) 并不能真正的中断线程,需要被调用的线程自己进行配合才行
- 如果线程处于被阻塞状态(例如处于sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt方法,那么该线程也会立即被阻塞状并抛出一个InterruptedException异常
- 中断只是一种协同机制,修改中断标识位仅此而已,不是立即stop打断
- sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在catch没有通过调用th.interrupt( )方法再次将中断标识位设置位true,这就是导致无限循环了
public static void m5()
{
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----isInterrupted() = true,程序结束。");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
//线程的中断标志位重新设置为false,无法停下,需要再次掉interrupt()设置true
Thread.currentThread().interrupt();//???????
e.printStackTrace();
}
System.out.println("------hello Interrupt");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
t1.interrupt();//修改t1线程的中断标志位为true
},"t2").start();
}
/**
*中断为true后,并不是立刻stop程序
*/
public static void m4()
{
//中断为true后,并不是立刻stop程序
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 300; i++) {
System.out.println("------i: " + i);
}
System.out.println("t1.interrupt()调用之后02: "+Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
System.out.println("t1.interrupt()调用之前,t1线程的中断标识默认值: "+t1.isInterrupted());
try { TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
//实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
t1.interrupt();
//活动状态,t1线程还在执行中
System.out.println("t1.interrupt()调用之后01: "+t1.isInterrupted());
try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
//非活动状态,t1线程不在执行中,已经结束执行了。
System.out.println("t1.interrupt()调用之后03: "+t1.isInterrupted());
}