JAVA中JUC多线程并发编程
第四章 线程中断,等待,唤醒与ThreadLocal
文章目录
一、等待和唤醒
3种让线程等待和唤醒的方法:
方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
java
//方式一
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
},"t2").start();
}
}
必须使用要求
- wait和notify方法必须要在同步块或者方法里面,且成对出现使用
- 先wait后notify才OK
方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
java
public class LockSupportDemo2
{
public static void main(String[] args)
{
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"start");
condition.await();
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
lock.lock();
try
{
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
},"t2").start();
}
}
使用要求:
- Condtion中的线程等待和唤醒方法之前,需要先获取锁
- 一定要先await后signal,不要反了
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程,通过park()和unpark(thread)方法来实现
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
LockSupport.park()时:permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。
LockSupport.unpark(thread):调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
java
public class LockSupportDemo3
{
public static void main(String[] args)
{
//正常使用+不需要锁块
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
},"t1");
t1.start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+" -----LockSupport.unparrk() invoked over");
}
}
二、中断
什么是中断
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的机制------中断。
中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
中断常用方法

使用中断标识停止线程
java
public class InterruptDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while(true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println("-----t1 线程被中断了,break,程序结束");
break;
}
System.out.println("-----hello");
}
}, "t1");
t1.start();
System.out.println("**************"+t1.isInterrupted());
//暂停5毫秒
try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
t1.interrupt();
System.out.println("**************"+t1.isInterrupted());
}
}
java
public class InterruptDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while(true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println("-----t1 线程被中断了,break,程序结束");
break;
}
//=================在上面加一段这个代码
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----hello");
}
}, "t1");
t1.start();
System.out.println("**************"+t1.isInterrupted());
//暂停5毫秒
try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
t1.interrupt();
System.out.println("**************"+t1.isInterrupted());
}
}
新增一段这个代码后,线程就会一直执行,不会停下来,如果想让他停止,需要加一段Thread.currentThread().interrupt();加在
java
public class InterruptDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(() -> {
while(true)
{
if(Thread.currentThread().isInterrupted())
{
System.out.println("-----t1 线程被中断了,break,程序结束");
break;
}
//=================在上面加一段这个代码
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("-----hello");
}
}, "t1");
t1.start();
System.out.println("**************"+t1.isInterrupted());
//暂停5毫秒
try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
t1.interrupt();
System.out.println("**************"+t1.isInterrupted());
}
}
原因

具体来说,当对一个线程,调用 interrupt() 时:
如果线程处于正常活动状态 ,那么会将该线程的中断标志设置为 true,仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态) ,在别的线程中调用当前线程对象的interrupt方法,
那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断
静态方法Thread.interrupted()
这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false
java
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 先给当前线程(main线程)设置一个中断标志
Thread.currentThread().interrupt();
System.out.println("=== 测试 Thread.interrupted() (静态方法) ===");
// 第一次调用:检测到中断,并清除
boolean result1 = Thread.interrupted();
System.out.println("第 1 次调用 interrupted(): " + result1); // 预期: true
// 第二次调用:因为刚才被清除了,所以检测不到
boolean result2 = Thread.interrupted();
System.out.println("第 2 次调用 interrupted(): " + result2); // 预期: false
System.out.println("\n=== 测试 thread.isInterrupted() (实例方法) ===");
// 为了对比,我们再次设置中断
Thread.currentThread().interrupt();
// 第一次调用:只检测,不清除
boolean result3 = Thread.currentThread().isInterrupted();
System.out.println("第 1 次调用 isInterrupted(): " + result3); // 预期: true
// 第二次调用:因为上次没清除,所以还是 true
boolean result4 = Thread.currentThread().isInterrupted();
System.out.println("第 2 次调用 isInterrupted(): " + result4); // 预期: true
}
}
三、ThreadLocal
是什么?
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本 。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
api方法

ThreadLocal源码分析:
聊ThreadLocal不得不提到其他两个


threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
JVM内部维护了**一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,**放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量 ,
人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
get()和set()方法
每个Thread对象维护着一个ThreadLocalMap的引用ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现"数据隔离",获取当前线程的局部变量值,不受其他线程影响~
问题
内存泄漏
内存泄漏是不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
为啥会出现内存泄漏呢?

说白了:map中的key用的弱引用,value用的强引用
又引入强,软,弱,虚引用在什么时候回收
强引用:不回收
软引用:内存不足即回收
弱引用:发现即回收
为啥key用弱引用呢


当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null 。
那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。
但是ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链
在实际使用中我们有时候会用线程池去维护我们的线程,为了复用线程是不会结束的,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remove()方法来删除它
应用场景
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景用空间换时间
解决线程安全问题(封装非线程安全的工具类)
这是最经典的应用场景。很多工具类(如 SimpleDateFormat、Random)不是线程安全的。如果在多线程环境下共享同一个实例,会导致数据错乱。
痛点:每次使用都 new 一个对象太浪费资源;加锁(synchronized)又会降低并发性能。
ThreadLocal 方案:为每个线程维护一个独立的实例,既保证了线程安全,又复用了对象,还避免了锁竞争。
java
public class DateUtils {
// 每个线程都会初始化一个属于自己的 SimpleDateFormat
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return DATE_FORMAT.get().format(date);
}
// 记得用完清理
public static void clear() {
DATE_FORMAT.remove();
}
}
跨方法传递上下文信息(隐式传参)
在复杂的业务链路中(尤其是 Web 应用),很多数据需要在 Controller、Service、Dao 层之间传递,比如用户 ID、订单 ID、权限信息等。
痛点:如果通过方法参数一层层传递,代码会非常冗余,耦合度高(所谓的"参数透传")。
ThreadLocal 方案:在请求入口(如拦截器/Filter)把数据存入 ThreadLocal,在业务层的任何地方直接获取,无需传参。
场景
用户上下文:Spring Security 的 SecurityContextHolder 就是基于 ThreadLocal 实现的,用于存储当前登录用户的信息。
全链路追踪:日志框架(如 Logback/Log4j2 的 MDC)利用 ThreadLocal 存储 TraceId,确保同一请求的所有日志都能带上同一个追踪 ID。
框架底层的资源管理(数据库连接/事务)
这是 ThreadLocal 在框架层面的核心应用。在 Spring 的事务管理中,为了保证同一个事务内的多次数据库操作使用的是同一个数据库连接,框架必须把连接"绑定"在当前线程上。
原理:
事务开启时,Spring 从连接池获取一个 Connection,并存入 ThreadLocal。
业务代码执行 SQL 时,MyBatis/Hibernate 从 ThreadLocal 中取出这个 Connection。
事务提交/回滚时,关闭连接并从 ThreadLocal 中移除。