钢结构网站,海口网站建设品牌大全,手机网站pc网站,怎么知道网站有没有备案1.线程和进程的区别#xff1f;
进程是正在运行程序的实例#xff0c;进程中包含了线程#xff0c;每个线程执行不同的任务不同的进程使用不同的内存空间#xff0c;在当前进程下的所有线程可以共享内存空间线程更轻量#xff0c;线程上下文切换成本一般上要比进程上下文…
1.线程和进程的区别
进程是正在运行程序的实例进程中包含了线程每个线程执行不同的任务不同的进程使用不同的内存空间在当前进程下的所有线程可以共享内存空间线程更轻量线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2. 并行和并发有什么区别
并发concurrent是同一时间应对dealing with多件事情的能力比如多个线程轮流使用一个或多个CPU
并行parallel是同一时间动手做doing多件事情的能力比如4核CPU同时执行4个线程
举例
家庭主妇做饭、打扫卫生、给孩子喂奶她一个人轮流交替做这多件事这时就是并发雇了3个保姆一个专做饭、一个专打扫卫生、一个专喂奶互不干扰这时是并行
3. 创建线程的四种方式
共有四种方式可以创建线程分别是继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程
详细创建方式参考下面代码
① 继承Thread类
public class MyThread extends Thread {Overridepublic void run() {System.out.println(MyThread...run...);}public static void main(String[] args) {// 创建MyThread对象MyThread t1 new MyThread() ;MyThread t2 new MyThread() ;// 调用start方法启动线程t1.start();t2.start();}}② 实现runnable接口
public class MyRunnable implements Runnable{Overridepublic void run() {System.out.println(MyRunnable...run...);}public static void main(String[] args) {// 创建MyRunnable对象MyRunnable mr new MyRunnable() ;// 创建Thread对象Thread t1 new Thread(mr) ;Thread t2 new Thread(mr) ;// 调用start方法启动线程t1.start();t2.start();}}③ 实现Callable接口
public class MyCallable implements CallableString {Overridepublic String call() throws Exception {System.out.println(MyCallable...call...);return OK;}public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建MyCallable对象MyCallable mc new MyCallable() ;// 创建FFutureTaskString ft new FutureTaskString(mc) ;// 创建Thread对象Thread t1 new Thread(ft) ;Thread t2 new Thread(ft) ;// 调用start方法启动线程t1.start();// 调用ft的get方法获取执行结果String result ft.get();// 输出System.out.println(result);}}④ 线程池创建线程
public class MyExecutors implements Runnable{Overridepublic void run() {System.out.println(MyRunnable...run...);}public static void main(String[] args) {// 创建线程池对象ExecutorService threadPool Executors.newFixedThreadPool(3);threadPool.submit(new MyExecutors()) ;// 关闭线程池threadPool.shutdown();}}4. runnable 和 callable 有什么区别
Runnable 接口run方法没有返回值Callable接口call方法有返回值是个泛型和Future、FutureTask配合可以用来获取异步执行的结果Callalbe接口支持返回执行结果需要调用FutureTask.get()得到此方法会阻塞主进程的继续往下执行如果不调用不会阻塞。Callable接口的call()方法允许抛出异常而Runnable接口的run()方法的异常只能在内部消化不能继续上抛
5. 线程的 run()和 start()有什么区别
start(): 用来启动线程通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码可以被调用多次。
6. 线程包括哪些状态状态之间是如何变化的? 状态之间是如何变化的 分别是
新建 当一个线程对象被创建但还未调用 start 方法时处于新建状态此时未与操作系统底层线程关联 可运行 调用了 start 方法就会由新建进入可运行此时与底层线程关联由操作系统调度执行 终结 线程内代码已经执行完毕由可运行进入终结此时会取消与底层线程关联 阻塞 当获取锁失败后由可运行进入 Monitor 的阻塞队列阻塞此时不占用 cpu 时间当持锁线程释放锁时会按照一定规则唤醒阻塞队列中的阻塞线程唤醒后的线程进入可运行状态 等待 当获取锁成功后但由于条件不满足调用了 wait() 方法此时从可运行状态释放锁进入 Monitor 等待集合等待同样不占用 cpu 时间当其它持锁线程调用 notify() 或 notifyAll() 方法会按照一定规则唤醒等待集合中的等待线程恢复为可运行状态 有时限等待 调用 sleep(long) 方法也会从可运行状态进入有时限等待状态但与 Monitor 无关不需要主动唤醒超时时间到自然恢复为可运行状态
7. 新建 T1、T2、T3 三个线程如何保证它们按顺序执行
在多线程中有多种方法让线程按特定顺序执行你可以用线程类的join()方法在一个线程中启动另一个线程另外一个线程完成该线程继续执行。
代码举例
为了确保三个线程的顺序你应该先启动最后一个(T3 调用T2T2调用T1)这样T1就会先完成而T3最后完成
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线程执行完毕以后再次执行该线程} catch (InterruptedException e) {e.printStackTrace();}System.out.println(t3);}) ;// 启动线程t1.start();t2.start();t3.start();}}8. notify()和 notifyAll()有什么区别
notifyAll唤醒所有wait的线程
notify只随机唤醒一个 wait 线程
9. 在 java 中 wait 和 sleep 方法的不同
共同点
wait() wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权进入阻塞状态
不同点 方法归属不同 sleep(long) 是 Thread 的静态方法而 wait()wait(long) 都是 Object 的成员方法每个对象都有 醒来时机不同 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来wait(long) 和 wait() 还可以被 notify 唤醒wait() 如果不唤醒就一直等下去它们都可以被打断唤醒 锁特性不同重点 wait 方法的调用必须先获取 wait 对象的锁搭配synchronized一起用而 sleep 则无此限制wait 方法执行后会释放对象锁允许其它线程获得该对象锁我放弃 cpu但你们还可以用而 sleep 如果在 synchronized 代码块中执行并不会释放对象锁我放弃 cpu你们也用不了
10. 如何停止一个正在运行的线程
有三种方式可以停止线程
使用退出标志使线程正常退出也就是当run方法完成后线程终止使用stop方法强行终止不推荐方法已作废使用interrupt方法中断线程
① 使用退出标志使线程正常退出。
public class MyInterrupt1 extends Thread {volatile boolean flag false ; // 线程执行的退出标记Overridepublic void run() {while(!flag) {System.out.println(MyThread...run...);try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 创建MyThread对象MyInterrupt1 t1 new MyInterrupt1() ;t1.start();// 主线程休眠6秒Thread.sleep(6000);// 更改标记为truet1.flag true ;}
}② 使用stop方法强行终止
public class MyInterrupt2 extends Thread {volatile boolean flag false ; // 线程执行的退出标记Overridepublic void run() {while(!flag) {System.out.println(MyThread...run...);try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 创建MyThread对象MyInterrupt2 t1 new MyInterrupt2() ;t1.start();// 主线程休眠2秒Thread.sleep(6000);// 调用stop方法t1.stop();}
}③ 使用interrupt方法中断线程。 打断阻塞线程 Thread t1 new Thread(() - {System.out.println(t1 正在运行...);try {Thread.sleep(5000); // 线程t1进入阻塞状态} catch (InterruptedException e) {e.printStackTrace(); // 中断时会抛出异常并进入这里}
}, t1);
t1.start();
Thread.sleep(500); // 主线程等待0.5秒保证t1已经进入阻塞状态
t1.interrupt(); // 打断线程t1
System.out.println(t1.isInterrupted()); // 打印t1的中断状态t1 正在运行...
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.itheima.basic.MyInterrupt3.lambda$main$0(MyInterrupt3.java:8)at java.lang.Thread.run(Thread.java:748)
true解释 线程 t1 在 sleep(5000) 时进入了阻塞状态。主线程在 0.5 秒后调用了 t1.interrupt()导致 t1 被唤醒并抛出 InterruptedException。捕获异常后t1 结束了执行并且我们通过 t1.isInterrupted() 打印了中断状态该方法返回 true表示 t1 确实被中断过。 打断正常线程 Thread t2 new Thread(() - {while (true) {Thread current Thread.currentThread();boolean interrupted current.isInterrupted(); // 检查当前线程是否被中断if (interrupted) {System.out.println(打断状态 interrupted); // 打印中断状态break; // 结束循环线程退出}}
}, t2);t2.start();
Thread.sleep(500); // 主线程等待0.5秒后中断t2
t2.interrupt(); // 如果此行取消注释将打断t2打断状态true解释 线程 t2 在循环中检测 isInterrupted() 的状态。一旦 t2.interrupt() 被调用isInterrupted() 返回 true并打印中断状态随后线程 t2 退出循环。
11. 讲一下synchronized关键字的底层原理
如下抢票的代码如果不加锁就会出现超卖或者一张票卖给多个人
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】其它线程再想获取这个【对象锁】时就会阻塞住
public class TicketDemo {static Object lock new Object();int ticketNum 10;public synchronized void getTicket() {synchronized (this) {if (ticketNum 0) {return;}System.out.println(Thread.currentThread().getName() 抢到一张票,剩余: ticketNum);// 非原子性操作ticketNum--;}}public static void main(String[] args) {TicketDemo ticketDemo new TicketDemo();for (int i 0; i 20; i) {new Thread(() - {ticketDemo.getTicket();}).start();}}}Monitor 被翻译为监视器是由jvm提供c语言实现
在代码中想要体现monitor需要借助javap命令查看clsss的字节码比如以下代码
public class SyncTest {static final Object lock new Object();static int counter 0;public static void main(String[] args) {synchronized (lock) {counter;}}
}找到这个类的class文件在class文件目录下执行javap -v SyncTest.class反编译效果如下 monitorenter 上锁开始的地方monitorexit 解锁的地方其中被monitorenter和monitorexit包围住的指令就是上锁的代码有两个monitorexit的原因第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁 在使用了synchornized代码块时需要指定一个对象所以synchornized也被称为对象锁
monitor主要就是跟这个对象产生关联如下图 Monitor内部具体的存储结构 Owner存储当前获取锁的线程的只能有一个线程可以获取 EntryList关联没有抢到锁的线程处于Blocked状态的线程 WaitSet关联调用了wait方法的线程处于Waiting状态的线程
具体的流程
代码进入synchorized代码块先让lock对象锁关联的monitor然后判断Owner是否有线程持有如果没有线程持有则让当前线程持有表示该线程获取锁成功如果有线程持有则让当前线程进入entryList进行阻塞如果Owner持有的线程已经释放了锁在EntryList中的线程去竞争锁的持有权非公平如果代码块中调用了wait()方法则会进去WaitSet中进行等待
参考回答 Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】 它的底层由monitor实现的monitor是jvm级别的对象 C实现线程获得锁需要使用对象锁关联monitor 在monitor内部有三个属性分别是owner、entrylist、waitset 其中owner是关联的获得锁的线程并且只能关联一个线程entrylist关联的是处于阻塞状态的线程waitset关联的是处于Waiting状态的线程
12. synchronized关键字的底层原理-进阶
Monitor实现的锁属于重量级锁你了解过锁升级吗 Monitor实现的锁属于重量级锁里面涉及到了用户态和内核态的切换、进程的上下文切换成本较高性能比较低。 在JDK 1.6引入了两种新型锁机制偏向锁和轻量级锁它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
2.2.1 对象的内存结构
在HotSpot虚拟机中对象在内存中存储的布局可分为3块区域对象头Header、实例数据Instance Data和对齐填充 我们需要重点分析MarkWord对象头
2.2.2 MarkWord hashcode25位的对象标识Hash码 age对象分代年龄占4位 biased_lock偏向锁标识占1位 0表示没有开始偏向锁1表示开启了偏向锁 thread持有偏向锁的线程ID占23位 epoch偏向时间戳占2位 ptr_to_lock_record轻量级锁状态下指向栈中锁记录的指针占30位 ptr_to_heavyweight_monitor重量级锁状态下指向对象监视器Monitor的指针占30位 我们可以通过lock的标识来判断是哪一种锁的等级
后三位是001表示无锁后三位是101表示偏向锁后两位是00表示轻量级锁后两位是10表示重量级锁
2.2.3 再说Monitor重量级锁
每个 Java 对象都可以关联一个 Monitor 对象如果使用 synchronized 给对象上锁重量级之后该对象头的Mark Word 中就被设置指向 Monitor 对象的指针 简单说就是每个对象的对象头都可以设置monoitor的指针让对象与monitor产生关联
2.2.4 轻量级锁
在很多的情况下在Java程序运行时同步块中的代码都是不存在竞争的不同的线程交替的执行同步块中的代码。这种情况下用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
static final Object obj new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();}
}public static void method2() {synchronized (obj) {// 同步块 B}
}加锁的流程
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恢复成为无锁状态。如果失败则膨胀为重量级锁。 2.2.5 偏向锁
轻量级锁在没有竞争时就自己这个线程每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头之后发现
这个线程 ID 是自己的就表示没有竞争不用重新 CAS。以后只要不发生竞争这个对象就归该线程所有
static final Object obj new Object();public static void m1() {synchronized (obj) {// 同步块 Am2();}
}public static void m2() {synchronized (obj) {// 同步块 Bm3();}
}public static void m3() {synchronized (obj) {}
}加锁的流程
1.在线程栈中创建一个Lock Record将其obj字段指向锁对象。 2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中同时也设置偏向锁的标识为101如果对象处于无锁状态则修改成功代表该线程获得了偏向锁。 3.如果是当前线程已经持有该锁了代表这是一次锁重入。设置Lock Record第一部分为null起到了一个重入计数器的作用。与轻量级锁不同的时这里不会再次进行cas操作只是判断对象头中的线程id是否是自己因为缺少了cas操作性能相对轻量级锁更好一些 解锁流程参考轻量级锁
2.2.6 参考回答
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
描述重量级锁底层使用的Monitor实现里面涉及到了用户态和内核态的切换、进程的上下文切换成本较高性能比较低。轻量级锁线程加锁的时间是错开的也就是没有竞争可以使用轻量级锁来优化。轻量级修改了对象头的锁标志相对重量级锁性能提升很多。每次修改都是CAS操作保证原子性偏向锁一段很长的时间内都只被一个线程使用锁可以使用了偏向锁在第一次获得锁时会有一个CAS操作之后该线程再获取锁只需要判断mark word中是否是自己的线程id即可而不是开销相对较大的CAS命令
一旦锁发生了竞争都会升级为重量级锁
13. 你谈谈 JMMJava 内存模型
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。 特点
所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量因为局部变量是线程私有的因此不存在竞争问题。每一个线程还存在自己的工作内存线程的工作内存保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读写)都必须在工作内存中完成而不能直接读写主内存中的变量不同线程之间也不能直接访问对方工作内存中的变量线程间变量的值的传递需要通过主内存完成。
14. CAS 你知道吗
CAS的全称是 Compare And Swap(比较再交换)它体现的一种乐观锁的思想在无锁情况下保证线程操作共享数据的原子性。
在JUC java.util.concurrent 包下实现的很多类都用到了CAS操作 AbstractQueuedSynchronizerAQS框架 AtomicXXX类
例子
我们还是基于刚才学习过的JMM内存模型进行说明
线程1与线程2都从主内存中获取变量int a 100,同时放到各个线程的工作内存中 一个当前内存值V、旧的预期值A、即将更新的值B当且仅当旧的预期值A和内存值V相同时将内存值修改为B并返回true否则什么都不做并返回false。如果CAS操作失败通过自旋的方式等待并再次尝试直到成功 线程1操作Vint a 100Aint a 100B修改后的值int a 101 (a) 线程1拿A的值与主内存V的值进行比较判断是否相等如果相等则把B的值101更新到主内存中 线程2操作Vint a 100Aint a 100B修改后的值int a 99(a–) 线程2拿A的值与主内存V的值进行比较判断是否相等(目前不相等因为线程1已更新V的值99)不相等则线程2更新失败 自旋锁操作 因为没有加锁所以线程不会陷入阻塞效率较高 如果竞争激烈重试频繁发生效率会受影响 需要不断尝试获取共享内存V中最新的值然后再在新的值的基础上进行更新操作如果失败就继续尝试获取新的值直到更新成功
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令 都是native修饰的方法由系统提供的接口执行并非java代码实现一般的思路也都是自旋锁实现 在java中比较常见使用有很多比如ReentrantLock和Atomic开头的线程安全类都调用了Unsafe中的方法
ReentrantLock中的一段CAS代码 15. 乐观锁和悲观锁 CAS 是基于乐观锁的思想最乐观的估计不怕别的线程来修改共享变量就算改了也没关系我吃亏点再重试呗。 synchronized 是基于悲观锁的思想最悲观的估计得防着其它线程来修改共享变量我上了锁你们都别想改我改完了解开锁你们才有机会。
16. 请谈谈你对 volatile 的理解
一旦一个共享变量类的成员变量、类的静态成员变量被volatile修饰之后那么就具备了两层语义
2.5.1 保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
一个典型的例子永不停止的循环
package com.itheima.basic;// 可见性例子
// -Xint
public class ForeverLoop {static boolean stop false;public static void main(String[] args) {new Thread(() - {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}stop true;System.out.println(modify stop to true...);}).start();foo();}static void foo() {int i 0;while (!stop) {i;}System.out.println(stopped... c: i);}
}当执行上述代码的时候发现foo()方法中的循环是结束不了的也就说读取不到共享变量的值结束循环。
主要是因为在JVM虚拟机中有一个JIT即时编辑器给代码做了优化。 上述代码 while (!stop) {
i;
}在很短的时间内这个代码执行的次数太多了当达到了一个阈值JIT就会优化此代码如下 while (true) {
i;
}当把代码优化成这样子以后及时stop变量改变为了false也依然停止不了循环 解决方案
第一
在程序运行的时候加入vm参数-Xint表示禁用即时编辑器不推荐得不偿失其他程序还要使用
第二
在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器问题就可以解决代码如下
static volatile boolean stop false;2.5.2 禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障阻止其他读写操作越过屏障从而达到阻止重排序的效果 在去获取上面的结果的时候有可能会出现4种情况
情况一先执行actor2获取结果—0,0(正常)
情况二先执行actor1中的第一行代码然后执行actor2获取结果—0,1(正常)
情况三先执行actor1中所有代码然后执行actor2获取结果—1,1(正常)
情况四先执行actor1中第二行代码然后执行actor2获取结果—1,0(发生了指令重排序影响结果)
解决方案
在变量上添加volatile禁止指令重排 序则可以解决问题 屏障添加的示意图 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
其他补充
我们上面的解决方案是把volatile加在了int y这个变量上我们能不能把它加在int x这个变量上呢
下面代码使用volatile修饰了x变量 屏障添加的示意图 这样显然是不行的主要是因为下面两个原则
写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
所以现在我们就可以总结一个volatile使用的小妙招
写变量让volatile修饰的变量的在代码最后位置读变量让volatile修饰的变量的在代码最开始位置
17. 什么是AQS
全称是 AbstractQueuedSynchronizer是阻塞式锁和相关的同步器工具的框架它是构建锁或者其他同步组件的基础框架
AQS与Synchronized的区别
synchronizedAQS关键字c 语言实现java 语言实现悲观锁自动释放锁悲观锁手动开启和关闭锁竞争激烈都是重量级锁性能差锁竞争激烈的情况下提供了多种解决方案
AQS常见的实现类 ReentrantLock 阻塞式锁 Semaphore 信号量 CountDownLatch 倒计时锁
工作机制:
在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态0表示无锁1表示有锁提供了基于 FIFO 的等待队列类似于 Monitor 的 EntryList条件变量来实现等待、唤醒机制支持多个条件变量类似于 Monitor 的 WaitSet 线程0来了以后去尝试修改state属性如果发现state属性是0就修改state状态为1表示线程0抢锁成功线程1和线程2也会先尝试修改state属性发现state的值已经是1了有其他线程持有锁它们都会到FIFO队列中进行等待FIFO是一个双向队列head属性表示头结点tail表示尾结点 如果多个线程共同去抢这个资源是如何保证原子性的呢 在去修改state状态的时候使用的cas自旋锁来保证原子性确保只能有一个线程修改成功修改失败的线程将会进入FIFO队列中等待
AQS是公平锁吗还是非公平锁 新的线程与队列中的线程共同来抢资源是非公平锁 新的线程到队列中等待只让队列中的head线程获取锁是公平锁 比较典型的AQS实现类ReentrantLock它默认就是非公平锁新的线程与队列中的线程共同来抢资源 18. ReentrantLock的实现原理
ReentrantLock翻译过来是可重入锁相对于synchronized它具备以下特点 可中断 可以设置超时时间 可以设置公平锁 支持多个条件变量 与synchronized一样都支持重入 ReentrantLock主要利用CASAQS队列来实现。它支持公平锁和非公平锁两者的实现类似
构造方法接受一个可选的公平参数默认非公平锁当设置为true时表示公平锁否则为非公平锁。公平锁的效率往往没有非公平锁的效率高在许多线程访问的情况下公平锁表现出较低的吞吐量。
查看ReentrantLock源码中的构造方法 提供了两个构造方法不带参数的默认为非公平
如果使用带参数的构造函数并且传的值为true则是公平锁
其中NonfairSync和FairSync这两个类父类都是Sync 而Sync的父类是AQS所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的 工作流程 线程来抢锁后使用cas的方式修改state状态修改状态成功为1则让exclusiveOwnerThread属性指向当前线程获取锁成功 假如修改状态失败则会进入双向队列中等待head指向双向队列头部tail指向双向队列尾部 当exclusiveOwnerThread为null的时候则会唤醒在双向队列中等待的线程 公平锁则体现在按照先后顺序获取锁非公平体现在不在排队的线程也可以抢锁
19. synchronized和Lock有什么区别 ? 语法层面 synchronized 是关键字源码在 jvm 中用 c 语言实现 Lock 是接口源码由 jdk 提供用 java 语言实现 使用 synchronized 时退出同步代码块锁会自动释放而使用 Lock 时需要手动调用 unlock 方法释放锁 功能层面 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能 Lock 提供了许多 synchronized 不具备的功能例如获取等待状态、公平锁、可打断、可超时、多条件变量 获取等待状态 Lock 可以提供当前线程的锁状态信息synchronized 无法实现。例子通过 ReentrantLock 获取等待线程数 import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final ReentrantLock lock new ReentrantLock();public void getLockInfo() {System.out.println(有多少线程在等待这个锁 lock.getQueueLength());}
}公平锁Lock 可以设置公平锁确保线程按请求顺序获取锁而 synchronized 是非公平锁。 import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final ReentrantLock fairLock new ReentrantLock(true); // true 表示公平锁public void fairLockMethod() {fairLock.lock();try {// 公平锁确保线程按请求顺序执行} finally {fairLock.unlock();}}
} 可打断Lock 提供了可中断的锁机制允许在获取锁时响应中断而 synchronized 无法中断等待锁的线程。例子使用 lockInterruptibly() 让线程在等待锁时能被中断 import java.util.concurrent.locks.ReentrantLock;public class InterruptibleLockExample {private final ReentrantLock lock new ReentrantLock();public void lockWithInterrupt() throws InterruptedException {lock.lockInterruptibly(); // 等待锁时可以被中断try {// do something} finally {lock.unlock();}}
} 可打断详细解释 什么是“可打断” 假设你正在排队买票突然接到一个紧急电话你可能需要离开队伍去处理电话。但如果你使用的是 synchronized那你就不能“离开队伍”只能一直等到轮到你为止。而使用 lockInterruptibly()就像排队时你可以被“叫走”即在等待的过程中可以被中断去做别的事情。 基本思路 synchronized如果你在等待锁你必须等到前面的任务完成释放锁期间不能“被叫走”不能中断。lockInterruptibly()如果你在等待锁可以在等待过程中被“叫走”被中断去处理别的紧急任务。 更简单的例子 假设有两个小朋友要玩同一个玩具 小朋友 A 拿到了玩具正在玩相当于获取了锁。小朋友 B 也想玩但只能等 A 玩完了等待锁。B 的妈妈打电话给他要他先回家吃饭中断请求。 如果 B 使用的是 synchronized他必须等 A 玩完才能离开哪怕妈妈在催他回家。 但如果 B 使用的是 lockInterruptibly()他可以在等待的时候听妈妈的话直接离开去吃饭不用一直等着玩具。 代码解释 这里用简单的代码演示“可打断”的行为 import java.util.concurrent.locks.ReentrantLock;public class SimpleInterruptExample {private final ReentrantLock lock new ReentrantLock();// 模拟小朋友玩玩具的方法public void play() throws InterruptedException {// 使用 lockInterruptibly() 可以在等待时被中断lock.lockInterruptibly();try {System.out.println(Thread.currentThread().getName() 拿到了玩具正在玩...);Thread.sleep(5000); // 模拟玩耍5秒} finally {lock.unlock(); // 玩完玩具后释放锁System.out.println(Thread.currentThread().getName() 玩完了离开了玩具。);}}public static void main(String[] args) {SimpleInterruptExample example new SimpleInterruptExample();// 创建两个小朋友的线程Thread child1 new Thread(() - {try {example.play();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() 被叫回家吃饭);}}, 小朋友A);Thread child2 new Thread(() - {try {example.play();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() 被叫回家吃饭);}}, 小朋友B);// 小朋友A 先拿到玩具开始玩child1.start();// 等A开始玩之后B也想玩但会被叫回家吃饭中断try {Thread.sleep(100); // 让小朋友A先拿到玩具} catch (InterruptedException e) {e.printStackTrace();}child2.start();// 模拟妈妈打电话让小朋友B回家中断线程Bchild2.interrupt(); // B 在等待时收到中断信号}
} 运行结果 小朋友A 拿到了玩具正在玩… 小朋友B 被叫回家吃饭 小朋友A 玩完了离开了玩具。 解读 小朋友A 拿到了玩具开始玩。小朋友B 想玩但是因为玩具被 A 拿着只能等。这时妈妈打电话让 B 回家 B 就被中断了不再等玩具直接回家退出了等待。当 A 玩完了玩具被释放但 B 已经不需要了。 总结 lockInterruptibly() 就像排队时你可以被“叫走”去处理更紧急的事情。即使还没等到锁也可以中途退出。如果你使用的是 synchronized就只能一直等到锁释放不能在等待过程中响应其他事件。 可超时Lock 提供了超时机制允许线程尝试在一定时间内获取锁超时后可放弃等待synchronized 没有这个功能。例子使用 tryLock(long time, TimeUnit unit) 设定超时时间 import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;public class TimeoutLockExample {private final ReentrantLock lock new ReentrantLock();public void lockWithTimeout() throws InterruptedException {if (lock.tryLock(3, TimeUnit.SECONDS)) { // 尝试获取锁最多等待3秒try {// 成功获取锁后执行} finally {lock.unlock();}} else {System.out.println(超时未能获取锁);}}
} 多条件变量 Lock 支持多个 Condition 对象每个条件变量可以独立地管理线程的等待和唤醒而 synchronized 只能通过 wait() 和 notify() 对所有线程进行管理。例子使用 Condition 实现多个条件变量 import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class MultiConditionExample {private final ReentrantLock lock new ReentrantLock();private final Condition condition1 lock.newCondition();private final Condition condition2 lock.newCondition();public void waitOnCondition1() throws InterruptedException {lock.lock();try {condition1.await(); // 等待 condition1 被唤醒} finally {lock.unlock();}}public void signalCondition1() {lock.lock();try {condition1.signal(); // 唤醒等待在 condition1 上的线程} finally {lock.unlock();}}
} ReentrantReadWriteLock适用于读多写少的场景支持读写锁分离。多个线程可以同时获取读锁但写锁是独占的。例子使用 ReentrantReadWriteLock 实现读写锁分离 import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock();private final ReentrantReadWriteLock.ReadLock readLock rwLock.readLock();private final ReentrantReadWriteLock.WriteLock writeLock rwLock.writeLock();public void readMethod() {readLock.lock();try {// 多线程可以同时读取} finally {readLock.unlock();}}public void writeMethod() {writeLock.lock();try {// 写操作只有一个线程能写} finally {writeLock.unlock();}}
}性能层面 在没有竞争时synchronized 做了很多优化如偏向锁、轻量级锁性能不赖在竞争激烈时Lock 的实现通常会提供更好的性能
20. 死锁产生的条件是什么
死锁一个线程需要同时获取多把锁这时就容易发生死锁 互斥条件Mutual Exclusion至少有一个资源必须处于非共享状态即一次只能被一个进程或线程占用。 请求与保持条件Hold and Wait进程或线程至少需要持有一个资源并且在等待其他资源时不释放已占有的资源。 不可剥夺条件No Preemption已分配给进程或线程的资源不能被强制性地剥夺只能由持有资源的进程或线程主动释放。 循环等待条件Circular Wait存在一个进程或线程的资源申请序列使得每个进程或线程都在等待下一个进程或线程所持有的资源。 例如 t1 线程获得A对象锁接下来想获取B对象的锁 t2 线程获得B对象锁接下来想获取A对象的锁 代码如下
package com.itheima.basic;import static java.lang.Thread.sleep;public class Deadlock {public static void main(String[] args) {Object A new Object();Object B new Object();Thread t1 new Thread(() - {synchronized (A) {System.out.println(lock A);try {sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println(lock B);System.out.println(操作...);}}}, t1);Thread t2 new Thread(() - {synchronized (B) {System.out.println(lock B);try {sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (A) {System.out.println(lock A);System.out.println(操作...);}}}, t2);t1.start();t2.start();}
}控制台输出结果 此时程序并没有结束这种现象就是死锁现象…线程t1持有A的锁等待获取B锁线程t2持有B的锁等待获取A的锁。
如何进行死锁诊断
当程序出现了死锁现象我们可以使用jdk自带的工具jps和 jstack
步骤如下
第一查看运行的线程 第二使用jstack查看线程运行的情况下图是截图的关键信息
运行命令jstack -l 46032 其他解决工具可视化工具
jconsole
用于对jvm的内存线程类 的监控是一个基于 jmx 的 GUI 性能监控工具
打开方式java 安装目录 bin目录下 直接启动 jconsole.exe 就行
VisualVM故障处理工具
能够监控线程内存情况查看方法的CPU时间和内存中的对 象已被GC的对象反向查看分配的堆栈
打开方式java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
21. 聊一下ConcurrentHashMap
ConcurrentHashMap 是一种线程安全的高效Map集合
底层数据结构 JDK1.7底层采用分段的数组链表实现 JDK1.8 采用的数据结构跟HashMap1.8的结构一样数组链表/红黑二叉树。
1 JDK1.7中concurrentHashMap
数据结构 提供了一个segment数组在初始化ConcurrentHashMap 的时候可以指定数组的长度默认是16一旦初始化之后中间不可扩容在每个segment中都可以挂一个HashEntry数组数组里面可以存储具体的元素HashEntry数组是可以扩容的在HashEntry存储的数组中存储的元素如果发生冲突则可以挂单向链表 存储流程 先去计算key的hash值然后确定segment数组下标再通过hash值确定hashEntry数组中的下标存储数据在进行操作数据的之前会先判断当前segment对应下标位置是否有线程进行操作为了线程安全使用的是ReentrantLock进行加锁如果获取锁是被会使用cas自旋锁进行尝试
2 JDK1.8中concurrentHashMap
在JDK1.8中放弃了Segment臃肿的设计数据结构跟HashMap的数据结构是一样的数组红黑树链表
采用 CAS Synchronized来保证并发安全进行实现 CAS控制数组节点的添加 synchronized只锁定当前链表或红黑二叉树的首节点只要hash不冲突就不会产生并发的问题 , 效率得到提升 22. 导致并发程序出现问题的根本原因是什么
22. Java程序中怎么保证多线程的执行安全
Java并发编程三大特性 原子性 可见性 有序性
1原子性
一个线程在CPU中操作不可暂停也不可中断要不执行完成要不不执行
比如如下代码能保证原子性吗 以上代码会出现超卖或者是一张票卖给同一个人执行并不是原子性的
解决方案
1.synchronized同步加锁
2.JUC里面的lock加锁 3内存可见性
内存可见性让一个线程对共享变量的修改对另一个线程可见
比如以下代码不能保证内存可见性 解决方案 synchronized volatile推荐 LOCK
3有序性
指令重排处理器为了提高程序运行效率可能会对输入代码进行优化它不保证程序中各个语句的执行先后顺序同代码中的顺序一致但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
还是之前的例子如下代码 解决方案
volatile
23. 说一下线程池的核心参数线程池的执行原理知道嘛
线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数 corePoolSize 核心线程数目 maximumPoolSize 最大线程数目 (核心线程临时线程的最大数目) keepAliveTime 生存时间 - 救急线程的生存时间生存时间内没有新任务此线程资源会释放 unit 时间单位 - 救急线程的生存时间单位如秒、毫秒等 workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建临时线程执行任务 threadFactory 线程工厂 - 可以定制线程对象的创建例如设置线程名字、是否是守护线程等 handler 拒绝策略 - 当所有线程都在繁忙workQueue 也放满时会触发拒绝策略
工作流程 1任务在提交的时候首先判断核心线程数是否已满如果没有满则直接添加到工作线程执行 2如果核心线程数满了则判断阻塞队列是否已满如果没有满当前任务存入阻塞队列 3如果阻塞队列也满了则判断线程数是否小于最大线程数如果满足条件则使用临时线程执行任务 如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程如果有则使用非核心线程执行任务 4如果所有线程都在忙着核心线程临时线程则走拒绝策略 拒绝策略 24. 线程池中有哪些常见的阻塞队列
workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建救急线程执行任务
比较常见的有4个用的最多是ArrayBlockingQueue和LinkedBlockingQueue
1.ArrayBlockingQueue基于数组结构的有界阻塞队列FIFO。
2.LinkedBlockingQueue基于链表结构的有界阻塞队列FIFO。
3.DelayedWorkQueue 是一个优先级队列它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue不存储元素的阻塞队列每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueueArrayBlockingQueue默认无界支持有界强制有界底层是单向链表底层是数组是懒惰的创建节点的时候添加数据提前初始化 Node 数组入队会生成新 NodeNode需要是提前创建好的两把锁头尾一把锁
左边是LinkedBlockingQueue加锁的方式右边是ArrayBlockingQueue加锁的方式
LinkedBlockingQueue读和写各有一把锁性能相对较好ArrayBlockingQueue只有一把锁读和写公用性能相对于LinkedBlockingQueue差一些 25. 如何确定核心线程数 IO 密集型任务建议使用 CPU 核数 * 2 的线程数。因为 IO 密集型任务在等待外部资源如网络、磁盘等时CPU 处于空闲状态。增加线程数可以在某些线程等待 I/O 时其他线程继续执行任务充分利用 CPU 资源。计算密集型任务建议使用 CPU 核数 1 的线程数。因为计算密集型任务完全占用 CPU线程数等于 CPU 核数能最大化资源利用。多一个线程可以在偶尔的线程切换或延迟时保持 CPU 始终有工作减少空闲时间。
26. 线程池的种类有哪些
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法常见就有四种 创建使用固定线程数的线程池 核心线程数与最大线程数一样没有救急线程 阻塞队列是LinkedBlockingQueue最大容量为Integer.MAX_VALUE 适用场景适用于任务量已知相对耗时的任务 案例 public class FixedThreadPoolCase {static class FixedThreadDemo implements Runnable{Overridepublic void run() {String name Thread.currentThread().getName();for (int i 0; i 2; i) {System.out.println(name : i);}}}public static void main(String[] args) throws InterruptedException {//创建一个固定大小的线程池核心线程数和最大线程数都是3ExecutorService executorService Executors.newFixedThreadPool(3);for (int i 0; i 5; i) {executorService.submit(new FixedThreadDemo());Thread.sleep(10);}executorService.shutdown();}}单线程化的线程池它只会用唯一的工作线程来执行任 务保证所有任务按照指定顺序(FIFO)执行 核心线程数和最大线程数都是1 阻塞队列是LinkedBlockingQueue最大容量为Integer.MAX_VALUE 适用场景适用于按照顺序执行的任务 案例 public class NewSingleThreadCase {static int count 0;static class Demo implements Runnable {Overridepublic void run() {count;System.out.println(Thread.currentThread().getName() : count);}}public static void main(String[] args) throws InterruptedException {//单个线程池核心线程数和最大线程数都是1ExecutorService exec Executors.newSingleThreadExecutor();for (int i 0; i 10; i) {exec.execute(new Demo());Thread.sleep(5);}exec.shutdown();}}可缓存线程池 核心线程数为0 最大线程数是Integer.MAX_VALUE 阻塞队列为SynchronousQueue:不存储元素的阻塞队列。 适用场景适合任务数比较密集但每个任务执行时间较短的情况 案例 public class CachedThreadPoolCase {static class Demo implements Runnable {Overridepublic void run() {String name Thread.currentThread().getName();try {//修改睡眠时间模拟线程执行需要花费的时间Thread.sleep(100);System.out.println(name 执行完了);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {//创建一个缓存的线程没有核心线程数最大线程数为Integer.MAX_VALUEExecutorService exec Executors.newCachedThreadPool();for (int i 0; i 10; i) {exec.execute(new Demo());Thread.sleep(1);}exec.shutdown();}}提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。 适用场景有定时和延迟执行的任务 案例 public class ScheduledThreadPoolCase {static class Task implements Runnable {Overridepublic void run() {try {String name Thread.currentThread().getName();System.out.println(name , 开始 new Date());Thread.sleep(1000);System.out.println(name , 结束 new Date());} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {//按照周期执行的线程池核心线程数为2最大线程数为Integer.MAX_VALUEScheduledExecutorService scheduledThreadPool Executors.newScheduledThreadPool(2);System.out.println(程序开始 new Date());/*** schedule 提交任务到线程池中* 第一个参数提交的任务* 第二个参数任务执行的延迟时间* 第三个参数时间单位*/scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);Thread.sleep(5000);// 关闭线程池scheduledThreadPool.shutdown();}}
27. 为什么不建议用Executors创建线程池
参考阿里开发手册《Java开发手册-嵩山版》,可能会导致内存溢出因为设置的最大线程数为Integer最大值或者阻塞队列没有上限导致内存溢出Out Of Memory
简单来说要么是最大线程无穷大要么是阻塞队列无穷大 28. 线程池使用场景Future
1. 多个服务独立运行
在一个电商网站中用户下单之后需要查询数据数据包含了三部分订单信息、包含的商品、物流信息这三块信息都在不同的微服务中进行实现的我们如何完成这个业务呢 在实际开发的过程中难免需要调用多个接口来汇总数据如果所有接口或部分接口的没有依赖关系就可以使用线程池future来提升性能 报表汇总
2. 异步调用(避免下一级方法影响上一级方法性能考虑)
1. 开启异步功能
首先仍然需要启用异步功能和之前一样在 Spring Boot 的主类上添加 EnableAsync 注解
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;SpringBootApplication
EnableAsync
public class AsyncApplication {public static void main(String[] args) {SpringApplication.run(AsyncApplication.class, args);}
}2. 创建自定义 ThreadPoolExecutor 线程池
我们将创建 ThreadPoolExecutor 并返回它作为 Executor 接口的实现。我们需要手动配置线程池的核心线程数、最大线程数、队列大小等。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executor;Configuration
public class AsyncConfig {Bean(name taskExecutor)public Executor taskExecutor() {// 创建ThreadPoolExecutorreturn new ThreadPoolExecutor(5, // 核心线程数10, // 最大线程数60, // 线程空闲时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue(100), // 任务队列new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略);}
}3. 使用异步方法
接下来在需要异步执行的方法上依然使用 Async 注解并指定线程池为我们自定义的 taskExecutor。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;Service
public class AsyncService {Async(taskExecutor)public void executeAsyncTask() {System.out.println(异步任务开始线程名称 Thread.currentThread().getName());try {Thread.sleep(2000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(异步任务结束线程名称 Thread.currentThread().getName());}
}4. 调用异步方法
在控制器中调用异步服务。和之前一样当访问 /async-task 时将触发异步任务任务将在后台线程池中执行。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;RestController
public class AsyncController {private final AsyncService asyncService;public AsyncController(AsyncService asyncService) {this.asyncService asyncService;}GetMapping(/async-task)public String triggerAsyncTask() {asyncService.executeAsyncTask();return 异步任务已经触发;}
}29. 如何控制某个方法允许并发访问线程的数量
Semaphore [ˈsɛməˌfɔr] 信号量是JUC包下的一个工具类我们可以通过其限制执行的线程数量达到限流的效果
当一个线程执行时先通过其方法进行获取许可操作获取到许可的线程继续执行业务逻辑当线程执行完成后进行释放许可操作未获取达到许可的线程进行等待或者直接结束。
Semaphore两个重要的方法
lsemaphore.acquire() 请求一个信号量这时候的信号量个数-1一旦没有可使用的信号量也即信号量个数变为负数时再次请求的时候就会阻塞直到其他线程释放了信号量
lsemaphore.release()释放一个信号量此时信号量个数1
线程任务类
public class SemaphoreCase {public static void main(String[] args) {// 1. 创建 semaphore 对象Semaphore semaphore new Semaphore(3);// 2. 10个线程同时运行for (int i 0; i 10; i) {new Thread(() - {try {// 3. 获取许可semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}try {System.out.println(running...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(end...);} finally {// 4. 释放许可semaphore.release();}}).start();}}}30. 谈谈你对ThreadLocal的理解 面试官谈谈你对ThreadLocal的理解 第一个是可以实现资源对象的线程隔离让每个线程各用各的资源对象避免争用引发的线程安全问题 第二个是实现了线程内的资源共享 面试官好的那你知道ThreadLocal的底层原理实现吗 ThreadLocal的底层原理是基于每个线程都有一个独立的存储区域ThreadLocalMap来实现的。以下是其核心机制 ThreadLocalMapThreadLocal的实际数据存储是基于ThreadLocalMap。每个线程对象Thread内部都有一个ThreadLocalMap这个ThreadLocalMap是用来为当前线程存储数据的。set()方法 当你调用ThreadLocal.set()时ThreadLocal对象会作为key实际的数据作为value存储在当前线程的ThreadLocalMap中。这个Map的结构是ThreadLocal, 值。不同的线程有独立的ThreadLocalMap因此相同的ThreadLocal对象在不同线程中的值是独立的。get()方法 当调用get()时系统会从当前线程的ThreadLocalMap中使用ThreadLocal对象作为key查找数据。如果找到key对应的值则返回该值如果没有找到则可以返回初始化的默认值。remove()方法 调用remove()会从当前线程的ThreadLocalMap中删除这个ThreadLocal对象对应的键值对以便释放不再需要的资源防止内存泄漏。 面试官那关于ThreadLocal会导致内存溢出这个事情了解吗 ThreadLocalMap 中的 key 被设计为弱引用它是被动的被GC调用释放key不过关键的是只有key可以得到内存释放而value不会因为value是一个强引用。 在使用ThreadLocal 时都把它作为静态变量即强引用因此无法被动依靠 GC 回收建议主动的remove 释放 key这样就能避免内存溢出。