1.线程与进程区别:
-
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
-
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
-
进程和线程的根本区别是进程是操作系统(OS)资源分配的基本单位,而线程是处理器(CPU)任务调度和执行的基本单位。
2.并发和并行的区别:
单核CPU
-
单核CPU下线程实际还是串行执行的
-
操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。
-
总结为一句话就是: 微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
3.创建线程的四种方式:
-
继承Thread类
public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } } class MyThread extends Thread{ @Override public void run() { super.run(); }
-
实现Runable接口
public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.start(); } } class MyThread implements Runnable{ @Override public void run() { }
-
实现Callable
public static void main(String[] args) throws Exception{ MyThread myThread = new MyThread(); FutureTask<String> stringFutureTask = new FutureTask<>(myThread); Thread thread = new Thread(stringFutureTask); thread.start(); String s = stringFutureTask.get(); System.out.println(s); } } class MyThread implements Callable<String> { @Override public String call() throws Exception { System.out.println("call接口使用。。。。。。"); return "OK"; }
-
线程池创建
public static void main(String[] args) throws Exception{ LinkedBlockingQueue<Runnable> objects = new LinkedBlockingQueue<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 3, 10, TimeUnit.SECONDS, objects, r ->new Thread("anmee"), new ThreadPoolExecutor.AbortPolicy() ); }
3.runnable和callable区别:
-
Runable没有返回值;callable有返回值,泛型,和Future、FutureTask配合可以用来获取异步执行的结果
-
callable接口支持返回值,需要有futuretask.get得到,此方法会阻塞主线程继续执行,如果不调用就不阻塞
-
callable接口的call方法允许抛出异常,而Runable接口的异常只能内部消化,不能向上抛出。
5.线程的run方法和start方法:
-
start方法原来启动线程,只能调用一次;而run方法封装了线程的执行逻辑,run方法可以执行多次
6.线程包括哪些状态:
-
Thread源码中定义了6种状态信息:
public enum State { /*当一个线程对象被创建,但还未调用 start 方法时处于新建状态 此时未与操作系统底层线程关联*/ NEW,就绪 /*调用了 start 方法,就会由新建进入可运行 此时与底层线程关联,由操作系统调度执行*/ RUNNABLE,可执行, /*当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用cpu 时间 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程, 唤醒后的线程进入可运行状态*/ BLOCKED,阻塞, /*当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行 状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等 待集合中的等待线程,恢复为可运行状态*/ WAITING,等待, /*当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可 运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu时间 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等 待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待 状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态*/ TIMED_WAITING,超时等待, /*线程内代码已经执行完毕,由可运行进入终结 此时会取消与底层线程关联*/ TERMINATED,销毁状态 }
7.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?:
-
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在 一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
-
注:调用join方法时,join的线程执行完了后本线程才会执行,
public class JoinTest { public static void main(String[] args) { // 创建线程对象 Thread t1 = new Thread(() -> { System.out.println("t1"); }) ; Thread t2 = new Thread(() -> { try { t1.join(); // 加入线程t1,只 有t1线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2"); }) ; Thread t3 = new Thread(() -> { try { t2.join(); // 加入线程 t2,只有t2线程执行完毕以后,再次执行该线程 }
8 notify()和 notifyAll()有什么区别?:
-
notifyAll:唤醒所有wait的线程
-
notify:只随机唤醒一个 wait 线程
9 在 java 中 wait 和 sleep 方法的不同?:
-
相同的:wait(),wait(long),sleep(long),的效果都是让当前线程暂时放弃CUP的使用权,进入阻塞状态.
-
不同点:1.方法归属不同,sleep属于Thread的静态方法,wait等都是Object的成员方法
-
相同点: 1.执行sleep(long)和wait(long)的线程都会在等待相应毫秒醒来,
2.wait(long)和wait()的线程都可以被notify唤醒,wait()如果唤醒就一直等下去。
3.它们都可以被打断。
-
锁特性不同(重点)
1.wait方法的调用必须获取wait对象的锁,而sleep无限制
2.wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
3.而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)
10 如何停止一个正在运行的线程?:
-
使用退出标准,使线程正常运行,即在run方法执行完后正常退出
-
使用stop方法强行终止(不推荐,方法已作废)线程执行到哪暂停到哪
-
使用interrupt方法中断线程,当需要抛出异常
一 public static void main(String[] args) throws Exception{ MyThread myThread = new MyThread(); myThread.start(); Thread.sleep(1000); myThread.flag = true; } } class MyThread extends Thread{ boolean flag = false; @Override public void run() { while (!flag){ System.out.println("正在running。。。。"); } System.out.println("退出循环线程结束"); }
二 public static void main(String[] args) throws Exception{ MyThread myThread = new MyThread(); myThread.start(); Thread.sleep(100); myThread.stop(); } } class MyThread extends Thread{ boolean flag = false; @Override public void run() { while (!flag){ System.out.println("正在running。。。。"); } System.out.println("退出循环线程结束"); } 方法不走完直接暂停
public static void main(String[] args) throws InterruptedException{ Thread t1 = new Thread(()->{ while (!Thread.interrupted()){ System.out.println("-before interrupt....."); } System.out.println("-after interrupt....."); },"t2"); t1.start(); Thread.sleep(1000); t1.interrupt(); System.out.println(t1.getName()); } 需要抛出InterruptedException异常
高阶:
1 讲一下synchronized关键字的底层原理?:
-
synchronized采用互斥的方式让同一时刻只能有一个线程能够抢到锁,其他线程在想获得到这个锁只能被阻塞住(对象级别)
public static void main(String[] args) throws InterruptedException{ Ticket ticket = new Ticket(); for (int i = 0;i < 20;i++){ new Thread(()->{ ticket.getTickets(); }).start(); } } } class Ticket { int tickets = 10; public void getTickets() { if (tickets <= 0) { return; } System.out.println(Thread.currentThread().getName() + "剩余" + tickets); tickets--; } 容易造成多个线程买一张票情况 在方法上面添加synchronized关键词
Monitor
synchronized的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获
得锁需要使用对象(锁)关联monitor
monitror由Owner,WaitSet,EntryList,
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
具体流程:
-
如果代码块调用了wait方法,则进入WaitSet中等待
-
如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线
程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
注:非公平锁为刚进来的线程就跟对列头的线程继续锁的争夺,公平锁为新来线程进入队列尾一起进行队列等待
-
如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
-
代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断
Owner是否有线程持有
总结:
-
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持
-
有【对象锁】它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获
得锁需要使用对象(锁)关联monitor
-
在monitor内部有三个属性,分别是owner、entrylist、waitset
-
其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的
是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
2 synchronized关键字的底层原理-进阶
-
Monitor实现的锁属于重量级锁,
-
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程
的上下文切换,成本较高,性能比较低。
-
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解
决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能
开销问题
1 对象的内存结构 :
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:
对象头:里面包含对象头和描述对象实例的具体数据
实例数据:里面存放空间变量
填充数据:如果(对象头+实例数据)不是8的整数倍,则通过对齐填充补齐
8倍数的优点:
-
内存对齐:处理器读取内存时,通常会要求数据的起始地址是特定字节的整数倍(比如4字节或8字节)。如果对象的起始地址不是这样对齐的,会增加处理器访问内存的时间,降低性能。
-
缓存对齐:现代处理器中有多级缓存,缓存行的大小通常是8字节或更大。如果对象大小不是缓存行大小的整数倍,可能会导致对象跨越多个缓存行,影响缓存的效率。
-
优化存储空间:对齐填充可以减少碎片化,使得内存的利用更加高效。
*2 轻量级锁:
-
在Java程序运行时,同步块中的代码都是不存在竞争的,不同
的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此
JVM引入了轻量级锁的概念。
加锁的流程:
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交
换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一
部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程:
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后
continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word
恢复成为无锁状态。如果失败则膨胀为重量级锁
5 偏向锁:
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到
对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,
这个对象就归该线程所有
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设
置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了
偏向锁
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一
部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再
次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,
性能相对轻量级锁更好一些
总结:
偏向锁:只被一个线程持有:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一
次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断
mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
轻量级锁:不同线程交替持有锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优
化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次
修改都是CAS操作,保证原子性
重量级锁:多线程竞争锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的
上下文切换,成本较高,性能比较低。
注意:锁升级流程:偏向锁->轻量锁->重量锁
3 .JMM(Java 内存模型):
内容:Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的
访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节
特点:
-
所有的共享变量(实例变量和类变量)都存储于主内存,不包含局部变量,局部变量是线程私有的,
-
每个线程里面都有自己的工作内存,工作内存里面存储这工作副本,工作副本跟主内存同步,避免了频繁于主内存交互
-
线程的读写都必须在本线程的工作内存中完成,各线程之间不允许变量交互,只能通过主内存进行交互
4.CAS:
基本:它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
自旋锁操作:因为没有加锁,所以线程不会陷入阻塞,效率较高如果竞争激烈,重试频繁发生,效率会受影响
需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功
底层实现:CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,是一个native方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现
public boolean compareAndSwap(int[] array, int expectedValue, int newValue) { synchronized (this) { if (array[0] == expectedValue) { array[0] = newValue; return true; } else { return false; } } }
5.AQS:
synchronized AQS
关键字,c++ 语言实现 java 语言实现
悲观锁,自动释放锁 悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差 锁竞争激烈的情况下,提供了多种解决方案
工作机制:
在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(FIFO是一个双向队列)
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
如果多个线程共同去抢这个资源是如何保证原子性的呢?:
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线
程修改成功,修改失败的线程将会进入FIFO队列中等待
注意:AQS本身并不具备公平锁或非公平锁的属性,它是一个框架,具体实现时可以根据需求选择如何管理线程的排队和唤醒顺序,从而决定实现的锁是公平的还是非公平的。
公平锁劣势:排队等待机制,竞争激烈时的线程唤醒频率,资源分配延迟,不利于并行性
6.ReentrantLock的实现原理:
特点:
-
可重入
-
支持多个变量
-
可以设置超时时间
-
可中断
-
可以设置公平锁
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者
的实现类似
7. synchronized和Lock有什么区别 ?:
-
语法层面:
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,
需要手动调用 unlock 方法释放锁
-
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平
锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现.
-
性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖。
在竞争激烈时,Lock 的实现通常会提供更好的性能,高扩展。
8. 死锁产生的条件是什么?:
-
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
9 如何进行死锁诊断?:
-
控制台:jsp,jstack
-
可视化界面:jconsolee,VisualVM,jdk自带
10.concurrentHashMap :
-
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
-
采用 CAS + Synchronized来保证并发安全进行实现CAS控制数组节点的添加
-
synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
11 导致并发程序出现问题的根本原因是什么:
-
原子性
-
可见性
-
有序性
原子性:
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
解决:1.synchronized:同步加锁
2.JUC里面的lock:加锁
内存可见性:
内存可见性:让一个线程对共享变量的修改对另一个线程可见
解决:synchronized,volatile(推荐),LOCK
有序性:
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保
证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终
执行结果和代码顺序执行的结果是一致的
解决:volatile
12.volatile:
-
保证线程间的可见性:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的
值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存
-
禁止进行指令重排序 :
有时候会因为调优问题对指令进行重新排序导致读写故障,这个可以阻止指令从排序
线程池:
1.线程七参数:
-
corePoolSize,核心线程数
-
maximumPoolSize,最大线程数
-
keepAliveTime,临时线程存活时间
-
unit时间单位
-
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满
会创建救急线程执行任务
-
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是
否是守护线程等
-
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝
策略
2.四种拒接策略:
-
1.AbortPolicy:直接抛出异常,默认策略
-
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
-
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
-
4.DiscardPolicy:直接丢弃任务;
3.线程池中有哪些常见的阻塞队列:
-
ArrayBlockingQueue:基于数组,有界,需要初始化node数据,一把锁
-
LinkedBlockingQueue :基于链表,默认无界,可有界,创节点时添加数据,2把锁
ArrayBlockingQueue是整体锁
LinkedBlockingQueue是2把锁,头尾各一个,分别负责读和写
4.如何确定核心线程数:
IO密集型任务:读写多,线程数为2n+1,并发不高、任务执行时间长
CPU密集型任务:计算多,线程数为n+1, 高并发、任务执行时间短防止线程进行上下切换造成资源浪费
5.常见的四种线程池:
1.newFixedThreadPool():
创建使用固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
适用:核心线程数与最大线程数一样,没有救急线程
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
适用场景:适用于任务量已知,相对耗时的任务
2.newSingleThreadExecutor():
单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按
照指定顺序(FIFO)执行
ExecutorService executorService = Executors.newSingleThreadExecutor();
适用:核心线程数和最大线程数都是1
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
适用场景:适用于按照顺序执行的任务
3.newCachedThreadPool();
可缓存的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
适用:核心线程数为0
最大线程数是Integer.MAX_VALUE
阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都
必须等待一个移出操作。
适用场景:适合任务数比较密集,但每个任务执行时间较短的情况
4.newScheduledThreadPool():
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool();
提供了“延迟”和“周期执行”功能的ThreadPoolExecutor
适用:有定时和延迟执行的任务
注意:
不建议适用Executors来创建线程池
why:newFixedThreadPool()和newSingleThreadExecutor()队列大小都是Intege.MAX_VALUE,导致堆积大量请求,容易导致oom
newScheduledThreadPool():允许创建的线程数为Intege.MAX_VALUE,容易创建大量线程,容易导致oom
6.CountDownLatch:
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成
倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)其中构造参数用来初始化等待计数值
await() 用来等待计数归零
countDown() 用来让计数减—
public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(3); new Thread(()->{ System.out.println(Thread.currentThread().getName()); latch.countDown(); },"t1").start(); new Thread(()->{ System.out.println(Thread.currentThread().getName()); latch.countDown(); },"t2").start(); }
7.线程场景题:
-
es批量导入:
当数据数太大了,同步到es的索引库,一次性读取数据肯定不行(oom异常),当
时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出
整体流程就是通过CountDownLatch+线程池配合去执行
流程:对整体数据进行分组,利用多线程进行批量加载,然后再最后进行批量加载进es的索引库
-
数据汇总:
可以引用多线程,进行批量加载信息,不用单线程进行依次加载,减少了整体时间
在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口
(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
报表汇总
-
案例二(异步调用):
在进行搜索的时候,需要保存用户的搜索记录,而搜索记录不能影响用户的正常
搜索,我们通常会开启一个线程去执行历史记录的保存,在新开启的线程在执行
的过程中,可以利用线程提交任务
8.如何控制某个方法允许并发访问线程的数量?
-
Semaphore [ˈsɛməˌfɔr] 信号量,是JUC包下的一个工具类,我们可以通过其限制
执行的线程数量,达到限流的效果
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行
业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等
待或者直接结束。
Semaphore两个重要的方法
lsemaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可
使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其
他线程释放了信号量)
lsemaphore.release():释放一个信号量,此时信号量个数+1
9.ThreadLocal的实现原理&源码解析:
static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws Exception{ new Thread(()->{ String name = Thread.currentThread().getName(); threadLocal.set(name); print(name); System.out.println("-after remove " + threadLocal.get()); },"t1").start(); new Thread(()->{ String name = Thread.currentThread().getName(); threadLocal.set(name); print(name); System.out.println("-after remove" + threadLocal.get()); },"t2").start(); } static void print(String name){ System.out.println(name + ":" + threadLocal.get()); threadLocal.remove(); }
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内
部的值,从而实现线程数据隔离
ThreadLocal的内存泄露问题:
四种引用:
-
强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果
一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现
OOM,也不会对其进行回收
String s = "123456";
-
弱引用:
弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区
域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回
收,无关内存区域是否足够,一旦发现则会被回收
Object o = new Object(); WeakReference weakReference = new WeakReference<>(o);
-
软引用:
软引用用于描述一些还有用但非必需的对象。当系统内存不足时,垃圾回收器会回收这些对象。在Java中,可以通过 SoftReference 类来创建软引用。使用软引用可以有效避免内存溢出的问题,因为当内存不足时,JVM会回收软引用对象,释放内存空间。
-
虚引用:
虚引用是一种比软引用和弱引用更弱的引用类型。虚引用的存在并不会对对象的生存时间产生影响,也无法通过虚引用获得被引用对象的实例。虚引用主要用于跟踪对象被垃圾回收的状态。在Java中,可以通过 PhantomReference 类来创建虚引用。虚引用通常与引用队列(ReferenceQueue)一起使用,当垃圾回收器准备回收一个对象时,如果发现它有虚引用,会将虚引用加入到引用队列中,以便进行进一步处理。
总的来说,软引用用于实现缓存等场景,而虚引用则用于管理对象被回收的状态。在实际开发中,了解并正确使用这两种引用类型可以帮助我们更好地管理内存,避免内存泄漏和性能问题。
ThreadLocal-内存泄露问题:
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承
了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量
的副本
查看ThreadLocal的源码
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry基础了WeakReference<ThreadLocal<?>>类是个虚引用类型,k为弱引用的Threadlocal实例,而底层value = v是强引用,当gc进行内存释放时,key释放了,value存在,就造成了内存泄漏,建议手动调用remove 方法进行手动释放;