文章目录
一、死锁
1.查看死锁
step1:jps命令查看进程号;
step2:jstack -l 进程号,查看是否发生死锁;
二、ThreadLocal
1.简介
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
note: 如果需要多个变量,那需要创建多个ThreadLocal。
2.数据结构

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap 由一个个Entry 对象构成。
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
Set
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
缺点:
ThreadLocalMap的key是弱引用类型。
使用完没有remove,可能导致内存泄露(GC后key被回收,value没有)。
父进程数据无法传递给子进程。
3.适用场景
1、线程间数据隔离
2、进行事务操作,用于存储线程事务信息。
3、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。
4.内存泄露

从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
为什么key为弱引用
如果hreadLocalMap使用ThreadLocal的强引用作为key,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
当ThreadLocalMap的key为弱引用,回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
综上,ThreadLocal内存泄漏的根源是:
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有
手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据。
- 将ThreadLocal变量定义成private static。
为什么要用static修饰ThreadLocal
没有被static修饰的ThreadLocal实例变量,会随着所在的类多次创建而被多次实例化。
当static是,ThreadLocal 生命延长,一直存在ThreadLocal的强引用,ThreadMap的key在线程生命期内始终有值,进而保证可以访问到vlaue进行访问或清除操作。
三、线程相关
1.线程通信
(1)volatile
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知(使用while(true)不停判断这个变量)并执行相应的业务。这也是最简单的一种实现方式。
(2)wait() 和 notify()
wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁。
调用 wait 方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。
notify 或 notifyAll 方法调用之后,等待线程依旧不会从 wait 返回,需要调用 notify 或 notifyAll 的线程释放锁之后,等待线程才有机会从 wait返回,继续执行之后的代码。
public class TestSync {
public static void main(String[] args) {
// 定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();// 唤醒B线程
}
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
(3)使用JUC工具类 CountDownLatch
CountDownLatch 可以初始化一个值(1), countDownLatch.countDown()可以让初始化的值减一,另一个线程使用while(true)不停执行 countDownLatch.await(),当计数器为0时,即可执行本线程工作。
四、并发编程三要素
1.原子性
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
2.可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
五、volatile与synchronized
1.synchronized
(1)简介
synchronized关键字同时满足原子性、可见性和有序性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
(2)三种使用方式
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
本文详细探讨了Java中的死锁检测、ThreadLocal的原理与应用、线程间的通信机制如volatile、wait/notify及CountDownLatch,并深入分析了并发编程的三大要素——原子性、可见性和有序性。此外,还对比了volatile与synchronized的区别,强调了ThreadLocal使用时防止内存泄露的重要性。

1364

被折叠的 条评论
为什么被折叠?



