当前位置: 首页 > news >正文

网站建设佰金手指科杰十八wordpress rss订阅地址

网站建设佰金手指科杰十八,wordpress rss订阅地址,湖南手机版建站系统开发,网站页面做海报用什么软件系列博客目录 文章目录 系列博客目录前言1.线程的基础知识1.1 线程和进程的区别#xff1f;难2频3面试文稿 1.2 并行和并发有什么区别#xff1f; 难1频1面试文稿 1.3 创建线程的四种方式 难2频4面试文稿 1.4 runnable 和 callable 有什么区别 难2频3面试文稿 1.5 线程的 run…系列博客目录 文章目录 系列博客目录前言1.线程的基础知识1.1 线程和进程的区别难2频3面试文稿 1.2 并行和并发有什么区别 难1频1面试文稿 1.3 创建线程的四种方式 难2频4面试文稿 1.4 runnable 和 callable 有什么区别 难2频3面试文稿 1.5 线程的 run()和 start()有什么区别难2频2面试文稿 1.6 线程包括哪些状态状态之间是如何变化的 难3频率4总结面试文稿 1.7 新建 T1、T2、T3 三个线程如何保证它们按顺序执行难2频3面试文稿 1.8 notify()和 notifyAll()有什么区别 2 2面试文稿 1.9 在 java 中 wait 和 sleep 方法的不同3 3面试文稿 1.10 如何停止一个正在运行的线程 2 2面试文稿 2.线程中并发锁2.1 讲一下synchronized关键字的底层原理 5 32.1.1 基本使用2.1.2 Monitor面试文稿 2.2 synchronized关键字的底层原理-进阶 Monitor实现的锁属于重量级锁你了解过锁升级吗?2.2.1 对象的内存结构2.2.2 MarkWord2.2.3 再说Monitor重量级锁2.2.4 轻量级锁2.2.5 偏向锁面试文稿 2.3 你谈谈 JMMJava 内存模型 3 3面试文稿 2.4 CAS 你知道吗3 22.4.1 概述及基本工作流程2.4.2 CAS 底层实现2.4.3 乐观锁和悲观锁是什么?(区别面试文稿 2.5 请谈谈你对 volatile 的理解 3 32.5.1 保证线程间的可见性2.5.2 禁止进行指令重排序面试文稿 2.6 什么是AQS2.6.1 概述2.6.2 工作机制面试文稿 2.7 ReentrantLock的实现原理 4 32.7.1 概述2.7.2 实现原理2.7.3 工作流程面试文稿 2.8 synchronized和Lock有什么区别 ? 4 4面试文稿 2.9 死锁产生的条件是什么 4 3面试文稿 2.10 如何进行死锁诊断 3 3面试文稿 2.11 ConcurrentHashMap(问到线程安全的时候会被顺便问到 3 4面试文稿 2.12 导致并发程序出现问题的根本原因是什么面试文稿 3.线程池3.1 说一下线程池的核心参数线程池的执行原理知道嘛3 4面试文稿 3.2 线程池中有哪些常见的阻塞队列 3 3面试文稿 3.3 如何确定核心线程数 4 3面试文稿 3.4 线程池的种类有哪些 3 3面试文稿 3.5 为什么不建议用Executors创建线程池 3 3面试文稿 4. 多线程使用场景问题4.1 线程池使用场景CountDownLatch、Future你们项目哪里用到了多线程 3 44.1.1 CountDownLatch4.1.2 案例一es数据批量导入4.1.3 案例二数据汇总4.1.4 案例三异步调用总结面试文稿 4.2 如何控制某个方法允许并发访问线程的数量 3 2面试文稿 5. 其他5.1 谈谈你对ThreadLocal的理解 3 45.1.1 概述5.1.2 ThreadLocal基本使用5.1.3 ThreadLocal的实现原理源码解析面试官比较关心5.1.4 ThreadLocal-内存泄露问题面试文稿 前言 线程是个难点但是面试官很爱问。分为四部分线程的基础知识简单好回答线程中并发安全这个以及之后都难起来了线程池使用场景。 1.线程的基础知识 1.1 线程和进程的区别难2频3 程序由指令和数据组成但这些指令要运行数据要读写就必须将指令加载至 CPU数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。 当一个程序被运行从磁盘加载这个程序的代码至内存这时就开启了一个进程。 多实例进程就是能被打开多次单实例进程就是只能被打开一次。 一个线程就是一个指令流将指令流中的一条条指令以一定的顺序交给 CPU 执行。一个进程之内可以分为一到多个线程。 面试文稿 进程是正在运行程序的实例进程中包含了线程每个线程执行不同的任务不同的进程使用不同的内存空间在当前进程下的所有线程可以共享内存空间线程更轻量线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程) 1.2 并行和并发有什么区别 难1频1 单核CPU 单核CPU下线程实际还是串行执行的操作系统中有一个组件叫做任务调度器将cpu的时间片windows下时间片最小约为 15 毫秒分给不同的程序使用只是由于cpu在线程间时间片很短的切换非常快人类感觉是同时运行的 。总结为一句话就是 微观串行宏观并行 一般会将这种线程轮流使用CPU的做法称为并发concurrent 多核CPU 每个核core都可以调度运行线程这时候线程可以是并行的。 并发concurrent是同一时间应对dealing with多件事情的能力 并行parallel是同一时间动手做doing多件事情的能力 举例 家庭主妇做饭、打扫卫生、给孩子喂奶她一个人轮流交替做这多件事这时就是并发家庭主妇雇了个保姆她们一起这些事这时既有并发也有并行这时会产生竞争例如锅只有一口一个人用锅时另一个人就得等待雇了3个保姆一个专做饭、一个专打扫卫生、一个专喂奶互不干扰这时是并行 面试文稿 现在都是多核CPU在多核CPU下并发是同一时间应对多件事情的能力多个线程轮流使用一个或多个CPU并行是同一时间动手做多件事情的能力4核CPU同时执行4个线程 1.3 创建线程的四种方式 难2频4 共有四种方式可以创建线程分别是继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程 详细创建方式参考下面代码 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();}}public class MyRunnable implements Runnable{Overridepublic void run() {//无法抛异常加上throw exception会直接报错。 可以加trycatchSystem.out.println(MyRunnable...run...);}public static void main(String[] args) {// 创建MyRunnable对象MyRunnable mr new MyRunnable() ;// 创建Thread对象 Thread t1 new Thread(mr) ;//用Thread包装MyRunnable的对象。Thread t2 new Thread(mr) ;// 调用start方法启动线程t1.start();t2.start();}}public class MyCallable implements CallableString {Overridepublic String call() throws Exception {//注意这里的返回值类型要与上面的泛型一致可以看出callable是有返回值的。System.out.println(MyCallable...call...);return OK;}public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建MyCallable对象MyCallable mc new MyCallable() ;// 创建F 包装了Callable的对象。FutureTaskString 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();}}面试文稿 在java中一共有四种常见的创建方式分别是继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。通常情况下我们项目中都会采用线程池的方式创建线程。 1.4 runnable 和 callable 有什么区别 难2频3 面试文稿 Runnable 接口run方法没有返回值Callable接口call方法有返回值是个泛型和Future、FutureTask配合可以用来获取异步执行的结果Callalbe接口支持返回执行结果需要调用FutureTask.get()得到此方法会阻塞主进程的继续往下执行如果不调用不会阻塞。Callable接口的call()方法允许抛出异常而Runnable接口的run()方法的异常只能在内部消化(可以加trycatch不能继续上抛 1.5 线程的 run()和 start()有什么区别难2频2 Thread t1 new Thread( name:t1){0verridepublic void run(){ system.out.println(running....); } t1.run(); t1.run(); t1.run();可以相当于调用普通方法。 t1.start() t1.start();不可以一个线程只能开启一次。面试文稿 start(): 用来启动线程通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。 run(): 封装了要被线程执行的代码可以被调用多次。 1.6 线程包括哪些状态状态之间是如何变化的 难3频率4 线程的状态可以参考JDK中的Thread类中的枚举State public enum State {/*** 尚未启动的线程的线程状态*/NEW,/*** 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行但它可能正在等待来自 * 操作系统的其他资源例如处理器。*/RUNNABLE,/*** 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调 * 用Object.wait后重新进入同步块/方法。*/BLOCKED,/*** 等待线程的线程状态。由于调用以下方法之一线程处于等待状态* Object.wait没有超时* 没有超时的Thread.join* LockSupport.park* 处于等待状态的线程正在等待另一个线程执行特定操作。* 例如一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify() * 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。*/WAITING,/*** 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一线程处于定 * 时等待状态* Thread.sleep* Object.wait超时* Thread.join超时* LockSupport.parkNanos* LockSupport.parkUntil* /ul*/TIMED_WAITING,/*** 已终止线程的线程状态。线程已完成执行*/TERMINATED;}public enum State {//尚未启动的线程的线程状态NEW,//可运行线程的线程状态。RUNNABLE.//线程阻塞等待监视器锁的线程状态。BLOCKED//等待线程的线程状态WAITING.//具有指定等待时间的等待线程的线程状态TIMED_WAITING,//已终止线程的线程状态。线程已完成执行TERMINATED; }代码如上一般流程如下包含新建状态和死亡状态。 就绪和运行是可执行状态。 分别是 新建 当一个线程对象被创建但还未调用 start 方法时处于新建状态此时未与操作系统底层线程关联 可运行 调用了 start 方法就会由新建进入可运行此时与底层线程关联由操作系统调度执行 终结 线程内代码已经执行完毕由可运行进入终结此时会取消与底层线程关联 阻塞 当获取锁失败后由可运行进入 Monitor 的阻塞队列阻塞此时不占用 cpu 时间当持锁线程释放锁时会按照一定规则唤醒阻塞队列中的阻塞线程唤醒后的线程进入可运行状态 等待 当获取锁成功后但由于条件不满足调用了 wait() 方法此时从可运行状态释放锁进入 Monitor 等待集合等待同样不占用 cpu 时间进入等待状态当其它持锁线程调用 notify() 或 notifyAll() 方法会按照一定规则唤醒等待集合中的等待线程恢复为可运行状态 有时限等待 当获取锁成功后但由于条件不满足调用了 wait(long) 方法此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待同样不占用 cpu 时间当其它持锁线程调用 notify() 或 notifyAll() 方法会按照一定规则唤醒等待集合中的有时限等待线程恢复为可运行状态并重新去竞争锁如果等待超时也会从有时限等待状态恢复为可运行状态并重新去竞争锁还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态但与 Monitor 无关不需要主动唤醒超时时间到自然恢复为可运行状态 总结 线程包括哪些状态 新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、计时等待(TIMED_WALTING)、终止(TERMINATED)线程状态之间是如何变化的 创建线程对象是新建状态 调用了start()方法转变为可执行状态 线程获取到了CPU的执行权执行结束是终止状态 在可执行状态的过程中如果没有获取CPU的执行权可能会切换其他状态如果没有获取锁(synchronized或lock)进入阻塞状态获得锁再切换为可执行状态 如果线程调用了wait()方法进入等待状态其他线程调用notify()唤醒后可切换为可执行状态 如果线程调用了sleep(50)方法进入计时等待状态到时间后可切换为可执行状态 面试文稿 在JDK中的Thread类中的枚举State里面定义了6种线程的状态分别是新建、可运行、终结、阻塞、等待和有时限等待六种。 关于线程的状态切换情况比较多。我分别介绍一下。 当一个线程对象被创建但还未调用 start 方法时处于新建状态调用了 start 方法就会由新建进入可运行状态。如果线程内代码已经执行完毕由可运行进入终止状态。当然这些是一个线程正常执行情况。 如果线程获取锁失败后会进入阻塞状态由可运行进入 Monitor 的阻塞队列阻塞只有当持锁线程释放锁时会按照一定规则唤醒阻塞队列中的阻塞线程唤醒后的线程进入可运行状态。 如果线程获取锁成功后但由于条件不满足调用了 wait() 方法此时从可运行状态释放锁进入等待状态当其它持锁线程调用 notify() 或 notifyAll() 方法会恢复为可运行状态。 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入计时有时限等待状态不需要主动唤醒超时时间到自然恢复为可运行状态。 1.7 新建 T1、T2、T3 三个线程如何保证它们按顺序执行难2频3 有很多方式以下是最简单方便的方式。 在多线程中有多种方法让线程按特定顺序执行你可以用线程类的join()方法在一个线程A中等待(或者寻找另一个线程B另外一个线程B完成(或者没有被运行后该线程A才能继续执行。 代码举例 为了确保三个线程的顺序你应该先启动最后一个(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();}}面试文稿 嗯~~我思考一下 适当的思考或想一下属于正常情况脱口而出反而太假[背诵痕迹] 可以这么做在多线程中有多种方法让线程按特定顺序执行可以用线程类的join()方法在一个线程中检测另一个线程另外一个线程完成该线程继续执行。 比如说 使用join方法T3调用T2T2调用T1这样就能确保T1就会先完成而T3最后完成 1.8 notify()和 notifyAll()有什么区别 2 2 public class WaitNotify {static Object lock new Object();public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() -{synchronized (lock){System.out.println(Thread.currentThread().getName() ...waiting);try{lock.wait();}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() ...被唤醒了);}},t1);Thread t2 new Thread(() -{synchronized (lock){System.out.println(Thread.currentThread().getName() ...waiting);try{lock.wait();}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() ...被唤醒了);}},t2);t1.start();t2.start();Thread.sleep(2000);synchronized (lock){lock.notify();//下面第二个结果是改为notifyAll()}} }t1...waiting t2...waiting t1...被唤醒了t1...waiting t2...waiting t1...被唤醒了 t2...被唤醒了面试文稿 notifyAll唤醒所有wait的线程 notify只随机唤醒一个 wait 线程 1.9 在 java 中 wait 和 sleep 方法的不同3 3 public class WaitSleepCase {static final Object LOCK new Object();public static void main(String[] args) throws InterruptedException {sleeping();}private static void illegalWait() throws InterruptedException {synchronized(LOCK){LOCK.wait();//这时候illegalWait()被main函数调用的话不会报错注意要在synchronized(LOCK)的情况下使用wati()。}}private static void waiting() throws InterruptedException {Thread t1 new Thread(() - {synchronized (LOCK) {//与下面的synchronized (LOCK)是同一把锁也就是只有一个synchronized (LOCK)代码块可以被执行try {get(t).debug(waiting...);LOCK.wait(5000L);//wait释放锁主线程才会获得锁。并且wait在5000L后会继续往下执行。} catch (InterruptedException e) {get(t).debug(interrupted...);e.printStackTrace();}}}, t1);t1.start();Thread.sleep(100);synchronized (LOCK) {main.debug(other...);}}private static void sleeping() throws InterruptedException {Thread t1 new Thread(() - {synchronized (LOCK) {try {get(t).debug(sleeping...);Thread.sleep(5000L);//sleep不会释放锁主线程才会获得锁。sleep在5s后运行完毕才会释放锁。} catch (InterruptedException e) {get(t).debug(interrupted...);e.printStackTrace();}}}, t1);t1.start();Thread.sleep(100);synchronized (LOCK) {main.debug(other...);}}}面试文稿 共同点 wait() wait(long即等待时间) 和 sleep(long即等待时间) 的效果都是让当前线程暂时放弃 CPU 的使用权进入阻塞状态 不同点 方法归属不同 sleep(long) 是 Thread 的静态方法而 wait()wait(long) 都是 Object 的成员方法每个对象都有 醒来时机不同 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来wait(long) 和 wait() 还可以被 notify 唤醒wait() 如果不唤醒就一直等下去sleep(long)必须有时间不会一直睡下去它们都可以被打断唤醒 锁特性不同重点 wait 方法的调用必须先获取 wait 对象的锁而 sleep 则无此限制wait 方法执行后会释放对象锁允许其它线程获得该对象锁我放弃 cpu但你们还可以用而 sleep 如果在 synchronized 代码块中执行并不会释放对象锁我放弃 cpu你们也用不了 1.10 如何停止一个正在运行的线程 2 2 第一种方式使用退出标志使线程正常退出。 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 ;} }输出如下 MyThread...run... 一开始进入while循环输出后睡3s到下一次循环 MyThread...run...输出完后继续睡3s程序运行至少6s此时flag被改while条件不满足了线程运行完毕第二种方式使用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方法中断线程 打断阻塞的线程(sleepwaitjoin)的线程线程会抛出InterruptedException异常打断正常的线程可以根据打断状态来标记是否退出线程 public class MyInterrupt3 {public static void main(String[] args) throws InterruptedException {//1.打断阻塞的线程Thread t1 new Thread(()-{System.out.println(t1 正在运行...);try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}, t1);t1.start();Thread.sleep(500);t1.interrupt(); * System.out.println(t1.isInterrupted());*///2.打断正常的线程Thread t2 new Thread(()-{while(true) {Thread current Thread.currentThread();boolean interrupted current.isInterrupted();if(interrupted) {//如果被打断了就会调用这个if里的代码 System.out.println(打断状态interrupted);break;}}}, t2);t2.start();Thread.sleep(500);t2.interrupt();} }面试文稿 有三种方式可以停止线程实际开发推荐第1、3种 使用退出标志使线程正常退出也就是当run方法完成后线程终止使用stop方法强行终止不推荐方法已作废使用interrupt方法中断线程 打断阻塞的线程(sleepwaitjoin)的线程线程会抛出InterruptedException异常打断正常的线程可以根据打断状态来标记是否退出线程 2.线程中并发锁 2.1 讲一下synchronized关键字的底层原理 5 3 2.1.1 基本使用 如下抢票的代码如果不加锁就会出现超卖或者一张票卖给多个人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();}} }不加锁结果如下图 不加锁加锁结果如下图 面试官会问synchronized 的底层。 2.1.2 Monitor monitorenter 上锁开始的地方monitorexit 解锁的地方其中被monitorenter和monitorexit包围住的指令就是上锁的代码有两个monitorexit的原因第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁 在使用了synchornized代码块时需要指定一个对象所以synchornized也被称为对象锁。 monitor主要就是跟这个对象产生关联如下图 Monitor内部具体的存储结构 Owner存储当前获取锁的线程的只能有一个线程可以获取EntryList关联没有抢到锁的线程处于Blocked状态的线程WaitSet关联调用了wait方法的线程处于Waiting状态的线程 具体的流程 代码进入synchorized代码块先让lock对象锁关联的monitor去判断Owner是否有线程持有这个锁。如果没有线程持有(也就是Owner为null)则让当前线程持有表示该线程获取锁成功如果有线程持有则让当前线程进入entryList进行阻塞如果Owner持有的线程已经释放了锁在EntryList中的线程去竞争锁的持有权非公平如果代码块中调用了wait()方法则会进去WaitSet中进行等待 面试文稿 Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。 Synchronized底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁如果某一个线程获得了锁在没有释放锁之前其他线程是不能或得到锁的。Synchronized 属于悲观锁。 Synchronized因为需要依赖于JVM级别的Monitor 相对性能也比较低。 monitor对象存在于每个Java对象的对象头中Synchronized锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因 monitor内部维护了三个变量 WaitSet保存处于Waiting状态的线程EntryList保存处于Blocked状态的线程Owner持有锁的线程 只有一个线程获取到的标志就是在monitor中设置成功了Owner一个monitor中只能有一个Owner。 在上锁的过程中如果有其他线程也来抢锁则进入EntryList 进行阻塞当获得锁的线程执行完了释放了锁就会唤醒EntryList 中等待的线程竞争锁竞争的时候是非公平的。 GPT悲观锁假设会发生冲突通过加锁来确保资源的独占。乐观锁假设不会发生冲突线程在操作资源时不加锁而是执行操作后检查是否发生冲突通常使用版本号、时间戳等机制来检查 2.2 synchronized关键字的底层原理-进阶 Monitor实现的锁属于重量级锁你了解过锁升级吗? Monitor实现的锁属于重量级锁里面涉及到了用户态和内核态的切换、进程的上下文切换成本较高性能比较低。(如果一个锁只有一个进程来重复获取释放它那样的话上下锁成本太高解决方法如下。在JDK 1.6引入了两种新型锁机制偏向锁和轻量级锁它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。 之前的时候我们默认对象锁管理moniter那是如何实现的呢实际上还是与java的对象有关 2.2.1 对象的内存结构 在HotSpot虚拟机(java一般都是用这个虚拟机中对象在内存中存储的布局可分为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程序运行时同步块中的代码都是不存在竞争的不同的线程交替的执行同步块中的代码。这种情况下用重量级锁是没必要的。(如下方代码如果一个线程调用了method1方法method1方法又调用了method2方法对锁进行了重入这时候由于是一个线程不存在竞争关系所以没有必要用重量级锁因此JVM引入了轻量级锁的概念。 static final Object obj new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();} }public static void method2() {synchronized (obj) {// 同步块 B} }一开始Object对象的内容是这样的还没有上锁。 1.在线程栈中创建一个Lock Record将其obj字段指向锁对象并且存储锁定对象的MarkWord。 2.如下图所示通过CAS指令将Lock Record的地址存储在对象头的mark word中数据进行交换如果对象处于无锁状态则修改成功代表该线程获得了轻量级锁。 3.如果是当前线程已经持有该锁了代表这是一次锁重入。(每次都要进行CAS操作但是重入的时候不用真正修改只要是添加一次记录并且设置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恢复成为无锁状态(再交换一次数据。如果失败则膨胀为重量级锁。(GPT:如果在 CAS 操作前其他线程已经修改了这个对象的 Mark Word比如已经将其修改为重量级锁的标志则 CAS 操作会失败。) 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操作性能相对轻量级锁更好一些。 面试文稿 Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。 重量级锁底层使用的Monitor实现里面涉及到了用户态和内核态的切换、进程的上下文切换成本较高性能比较低。 轻量级锁线程加锁的时间是错开的也就是没有竞争可以使用轻量级锁来优化。轻量级修改了对象头的锁标志相对重量级锁性能提升很多。每次修改都是CAS操作保证原子性。 偏向锁一段很长的时间内都只被一个线程使用锁可以使用了偏向锁在第一次获得锁时会有一个CAS操作之后该线程再获取锁只需要判断mark word中是否是自己的线程id即可而不是开销相对较大的CAS命令。 一旦锁发生了竞争都会升级为重量级锁。 2.3 你谈谈 JMMJava 内存模型 3 3 JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。 Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。 特点 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量因为局部变量是线程私有的因此不存在竞争问题。每一个线程还存在自己的工作内存线程的工作内存保留了被线程使用的变量的工作副本只能被线程自己本身所调用不存在安全问题。线程对变量的所有的操作(读写)都必须在工作内存中完成而不能直接读写主内存中的变量不同线程之间也不能直接访问对方工作内存中的变量线程间变量的值的传递需要通过主内存完成。 总结 你谈谈 JMM(Java内存模型) JMM(ava Memory Model)Java内存模型定义了共享内存中多线程程序读写操作的行为规范通过这些规则来规范对内存的读写操作从而保证指令的正确性JMM把内存分为两块一块是私有线程的工作区域(工作内存)一块是所有线程的共享区域(主内存)线程跟线程之间是相互隔离线程跟线程交互需要通过主内存 面试文稿 Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则以及这些变量在JVM中是如何被存储和读取的涉及到一些底层的细节。 这个模型有几个核心的特点。首先所有的共享变量包括实例变量和类变量都被存储在主内存中也就是计算机的RAM。需要注意的是局部变量并不包含在内因为它们是线程私有的所以不存在竞争问题。 其次每个线程都有自己的工作内存这里保留了线程所使用的变量的工作副本。这意味着线程对变量的所有操作无论是读还是写都必须在自己的工作内存中完成而不能直接读写主内存中的变量。 最后不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值那么这个过程必须通过主内存来完成。 2.4 CAS 你知道吗3 2 2.4.1 概述及基本工作流程 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的值101)不相等则线程2更新失败 开始执行自旋锁操作即不断拿去新的共享变量更改后对比旧值看看是否可以把自己更改后的值放到内存中。 自旋锁操作 因为没有加锁所以线程不会陷入阻塞效率较高如果竞争激烈重试频繁发生效率会受影响 需要不断尝试获取共享内存V中最新的值然后再在新的值的基础上进行更新操作如果失败就继续尝试获取新的值直到更新成功 2.4.2 CAS 底层实现 CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令(如下图所示。 都是native修饰的方法由系统提供的接口执行并非java代码实现一般的思路也都是自旋锁实现 在java中比较常见使用有很多比如ReentrantLock和Atomic开头的线程安全类都调用了Unsafe中的方法 ReentrantLock中的一段CAS代码 当前值对应前面的 内存总中的值V期望的值对应A更行后的值对应B。 2.4.3 乐观锁和悲观锁是什么?(区别 面试官可能根据CAS是乐观锁来问什么是乐观悲观锁。 CAS 是基于乐观锁的思想最乐观的估计不怕别的线程来修改共享变量就算改了也没关系我吃亏点再重试呗。synchronized 是基于悲观锁的思想最悲观的估计得防着其它线程来修改共享变量我上了锁你们都别想改我改完了解开锁你们才有机会。 面试文稿 CAS的全称是 Compare And Swap(比较再交换);它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。 CAS使用到的地方很多AQS框架(Abstract Queued Synchronizer)、AtomicXXX类在操作共享变量的时候使用的自旋锁效率上更高一些CAS的底层是调用的Unsafe类中的方法都是操作系统提供的其他语言实现 2.5 请谈谈你对 volatile 的理解 3 3 一旦一个共享变量类的成员变量、类的静态成员变量被volatile修饰之后那么就具备了两层语义 2.5.1 保证线程间的可见性 保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。用 volatile 修饰共享变量能够防止编译器等优化发生让一个线程对共享变量的修改对另一个线程可见。 举个例子如下有三个线程。 线程1修改了变量之后线程2是可以读到的为什么线程3不行呢 问题分析主要是因为在JVM虚拟机中有一个JT(即时编译器)给代码做了优化。 解决方案 第一在程序运行的时候加入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修饰的变量的在代码最开始位置 面试文稿 volatile 是一个关键字可以修饰类的成员变量、类的静态成员变量主要有两个功能 第一保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。 第二 禁止进行指令重排序可以保证代码执行有序性。底层实现原理是添加了一个内存屏障通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化 2.6 什么是AQS 2.6.1 概述 全称是 AbstractQueuedSynchronizer是阻塞式锁和相关的同步器工具的框架它是构建锁或者其他同步组件的基础框架 。其实就是用作锁。 AQS与Synchronized的区别 AQS常见的实现类 ReentrantLock 阻塞式锁Semaphore 信号量CountDownLatch 倒计时锁 2.6.2 工作机制 在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它默认就是非公平锁新的线程与队列中的线程共同来抢资源。 面试文稿 什么是AQS? 是多线程中的队列同步器。是一种锁机制它是做为一个基础框架使用的像ReentrantLock、Semaphore(/ˈseməfɔː(r)/都是基于AQS实现的AQS内部维护了一个先进先出的双向队列队列中存储排队的线程。在AQS内部还有一个属性state这个state就相当于是一个资源默认是0(无锁状态)如果队列中的有一个线程修改成功了state为1则当前线程就相等于获取了资源。在对state修改的时候使用的cas操作保证多个线程修改的情况下原子性。 2.7 ReentrantLock的实现原理 4 3 2.7.1 概述 ReentrantLock翻译过来是可重入锁相对于synchronized它具备以下特点 可中断可以设置超时时间可以设置公平锁支持多个条件变量与synchronized一样都支持重入 2.7.2 实现原理 ReentrantLock主要利用CASAQS队列来实现。它支持公平锁和非公平锁两者的实现类似。 构造方法接受一个可选的公平参数默认非公平锁当设置为true时表示公平锁否则为非公平锁。公平锁的效率往往没有非公平锁的效率高在许多线程访问的情况下公平锁表现出较低的吞吐量。 查看ReentrantLock源码中的构造方法 提供了两个构造方法不带参数的默认为非公平。 如果使用带参数的构造函数并且传的值为true则是公平锁。 其中NonfairSync和FairSync这两个类父类都是Sync而Sync的父类是AQS(如下图所示所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的。 2.7.3 工作流程 线程来抢锁后使用cas的方式修改state状态修改状态成功为1则让exclusiveOwnerThread属性指向当前线程获取锁成功。假如修改状态失败则会进入双向队列中等待head指向双向队列头部tail指向双向队列尾部当exclusiveOwnerThread为null的时候则会唤醒在双向队列中等待的线程公平锁则体现在按照先后顺序获取锁非公平体现在不在排队的线程也可以抢锁。 面试文稿 ReentrantLock是一个可重入锁: 一个线程调用 lock 方法获取了锁之后再次调用 lock是不会再阻塞内部直接增加重入次数就行了标识这个线程已经重复获取一把锁而不需要等待锁的释放。 ReentrantLock是属于juc包下的类跟synchronized一样都是悲观锁。属于api层面的锁通过lock()用来获取锁unlock()释放锁。 它的底层实现原理主要利用CASAQS队列来实现。它支持公平锁和非公平锁两者的实现类似。 构造方法接受一个可选的公平参数默认非公平锁当设置为true时表示公平锁否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。 2.8 synchronized和Lock有什么区别 ? 4 4 要对两个所有充分的认识才能回答好。 第一语法层面 synchronized 是关键字源码在 jvm 中用 c 语言实现退出同步代码块锁会自动释放Lock 是接口源码由 jdk 提供用 java 语言实现需要手动调用 unlock 方法释放锁 第二功能层面 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能例如获取等待状态、公平锁、可(在等待过程中被)打断、可超时、多条件变量同时Lock 可以实现不同的场景如 ReentrantLock ReentrantReadWriteLock。 /*** ClassName: LockInterruptiblyDemo* Package: PACKAGE_NAME* Description:** Author 醒了就刷牙* Create 2024/12/21 12:23* Version 1.0*/ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.*;import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockTest {//创建锁对象static ReentrantLock locknew ReentrantLock();//条件1static Condition c1 lock.newCondition();static Condition c2 lock.newCondition();public static void main(String[] args) throws InterruptedException { // lockInterrupt();time0utLock();}public static void time0utLock()throws InterruptedException {Thread t1 new Thread(() - {//设置trylock的时间使其一直尝试如果不设置就是尝试一下不满足的话直接执行获取不到锁的代码。try {if(!lock.tryLock(3000, TimeUnit.SECONDS)){System.out.println(t1-获取锁失败);return;}} catch (InterruptedException e) {throw new RuntimeException(e);}try {System.out.println(t1线程-获得了锁);}finally {lock.unlock();}},t1);lock.lock();System.out.println(主线程获得了锁);t1.start();try {Thread.sleep(4000);}finally {lock.unlock();}}public static void lockInterrupt()throws InterruptedException {Thread t1new Thread(()-{try {//开启可中断的锁lock.lockInterruptibly();}catch(InterruptedException e){e.printStackTrace();System.out.println(等待的过程中被打断);return;}try {System.out.println(Thread.currentThread().getName(),获得了锁);} finally {lock.unlock();}},t1);lock.lock();System.out.println(主线程获得了锁);t1.start();try {Thread.sleep(1000);t1.interrupt();System.out.println(主线程执行打断);}finally {lock.unlock();}} }多条件变量 就是 一个线程中使用 c1.await()进入等待等其他进程调用c1.signal()唤醒它 一个c1.signal只能唤醒一个由于c1进入等待的进程signalAll()可以唤醒所有。第三性能层面 在没有竞争时synchronized 做了很多优化如偏向锁、轻量级锁性能不赖。在竞争激烈时Lock 的实现通常会提供更好的性能。 统合来看需要根据不同的场景来选择不同的锁的使用。 面试文稿 第一语法层面 synchronized 是关键字源码在 jvm 中用 c 语言实现退出同步代码块锁会自动释放Lock 是接口源码由 jdk 提供用 java 语言实现需要手动调用 unlock 方法释放锁 第二功能层面 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能例如获取等待状态、公平锁、可打断、可超时、多条件变量同时Lock 可以实现不同的场景如 ReentrantLock ReentrantReadWriteLock 第三性能层面 在没有竞争时synchronized 做了很多优化如偏向锁、轻量级锁性能不赖在竞争激烈时Lock 的实现通常会提供更好的性能 统合来看需要根据不同的场景来选择不同的锁的使用。 2.9 死锁产生的条件是什么 4 3 死锁一个线程需要同时获取多把锁这时就容易发生死锁 例如 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的锁。 面试文稿 嗯是这样的一个线程需要同时获取多把锁这时就容易发生死锁举个例子来说 t1 线程获得A对象锁接下来想获取B对象的锁 t2 线程获得B对象锁接下来想获取A对象的锁 这个时候t1线程和t2线程都在互相等待对方的锁就产生了死锁。 2.10 如何进行死锁诊断 3 3 当程序出现了死锁现象我们可以使用jdk自带的工具jps和 jstack 步骤如下 第一查看运行的线程 第二使用jstack查看线程运行的情况下图是截图的关键信息 运行命令jstack -l 46032 其他解决工具可视化工具 jconsole 用于对jvm的内存线程类 的监控是一个基于 jmx 的 GUI 性能监控工具 打开方式java 安装目录 bin目录下 直接启动 jconsole.exe 就行 VisualVM故障处理工具 能够监控线程内存情况查看方法的CPU时间和内存中的对 象已被GC的对象反向查看分配的堆栈 打开方式java 安装目录 bin目录下 直接启动 jvisualvm.exe就行 面试文稿 死锁产生的条件是什么? 一个线程需要同时获取多把锁这时就容易发生死锁 如何进行死锁诊断? 当程序出现了死锁现象我们可以使用jdk自带的工具:jps((Java Virtual Machine Process Status Tool))和jstackjps:输出JVM中运行的进程状态信息jstack:查看java进程内线程的堆栈信息查看日志检查是否有死锁如果有死锁现象需要查看具体代码分析后可修复可视化工具jconsole、VisualVM也可以检查死锁问题 2.11 ConcurrentHashMap(问到线程安全的时候会被顺便问到 3 4 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不冲突就不会产生并发的问题 , 效率得到提升 面试文稿 ConcurrentHashMap 是一种线程安全的高效Map集合jdk1.7和1.8也做了很多调整。 JDK1.7的底层采用是分段的数组链表 实现JDK1.8 采用的数据结构跟HashMap1.8的结构一样数组链表/红黑二叉树。 在jdk1.7中 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似是一 种数组和链表结构一个 Segment 包含一个 HashEntry 数组每个 HashEntry 是一个链表结构 的元素每个 Segment 守护着一个HashEntry数组里的元素当对 HashEntry 数组的数据进行修改时必须首先获得对应的 Segment的锁。 Segment 是一种可重入的锁 ReentrantLock每个 Segment 守护一个HashEntry 数组里得元 素当对 HashEntry 数组的数据进行修改时必须首先获得对应的 Segment 锁 在jdk1.8中的ConcurrentHashMap 做了较大的优化性能提升了不少。首先是它的数据结构与jdk1.8的hashMap数据结构完全一致。其次是放弃了Segment臃肿的设计取而代之的是采用Node CAS Synchronized来保证并发安全进行实现synchronized只锁定当前链表或红黑二叉树的首节点这样只要hash不冲突就不会产生并发 , 效率得到提升 2.12 导致并发程序出现问题的根本原因是什么 面试文稿 Java并发编程有三大核心特性分别是原子性、可见性和有序性。 首先原子性指的是一个线程在CPU中的操作是不可暂停也不可中断的要么执行完成要么不执行。比如一些简单的操作如赋值可能是原子的但复合操作如自增就不是原子的。为了保证原子性我们可以使用synchronized关键字或JUC里面的Lock来进行加锁。 其次可见性是指让一个线程对共享变量的修改对另一个线程可见。由于线程可能在自己的工作内存中缓存共享变量的副本因此一个线程对共享变量的修改可能不会立即反映在其他线程的工作内存中。为了解决这个问题我们可以使用synchronized关键字、volatile关键字或Lock来确保可见性。 最后有序性是指处理器为了提高程序运行效率可能会对输入代码进行优化导致程序中各个语句的执行先后顺序与代码中的顺序不一致。虽然处理器会保证程序最终执行结果与代码顺序执行的结果一致但在某些情况下我们可能需要确保特定的执行顺序。为了解决这个问题我们可以使用volatile关键字来禁止指令重排。 3.线程池 为什么要用线程池呢因为我们每创建一个线程都会导致占用一些内存如果我们创建很多线程如果创建很多可能会导致线程溢出我们的内存毕竟是有限的同一时刻一个CPU只能处理一个线程如果大量的请求创建了大量的线程都没有CPU的执行权这些线程会进行等待会导致大量线程之间的切换也会导致性能变慢。一般在项目开发过程中一般都会使用线程池来创建并且管理线程。这一块和实际开发关系很大面试官比较喜欢问。 3.1 说一下线程池的核心参数线程池的执行原理知道嘛3 4 线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数 corePoolSize 核心线程数目maximumPoolSize 最大线程数目 (核心线程救急线程的最大数目)keepAliveTime 生存时间 - 救急线程的生存时间生存时间内没有新任务此线程资源会释放unit 时间单位 - 救急线程的生存时间单位如秒、毫秒等workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建救急线程执行任务threadFactory 线程工厂 - 可以定制线程对象的创建例如设置线程名字、是否是守护线程等handler 拒绝策略 - 当所有线程(核心和救急都在繁忙workQueue 也放满时会触发拒绝策略 线程池工作流程/执行原理 任务在提交的时候首先判断核心线程数是否已满如果没有满则直接添加到工作线程执行如果核心线程数满了则判断阻塞队列是否已满如果没有满当前任务存入阻塞队列如果阻塞队列也满了则判断线程数是否小于最大线程数如果满足条件则使用临时线程执行任务 如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程如果有则使用非核心线程执行任务(救急线程非核心线程临时线程)如果所有线程都在忙着核心线程临时线程则走拒绝策略 拒绝策略 1.AbortPolicy直接抛出异常默认策略 2.CallerRunsPolicy用调用者所在的线程来执行任务 3.DiscardOldestPolicy丢弃阻塞队列中靠最前的任务并执行当前任务 4.DiscardPolicy直接丢弃任务 代码示例 public class TestThreadPoolExecutor {static class MyTask implements Runnable {private final String name;private final long duration;public MyTask(String name) {this(name, 0);}public MyTask(String name, long duration) {this.name name;this.duration duration;}Overridepublic void run() {try {LoggerUtils.get(myThread).debug(running... this);Thread.sleep(duration);} catch (InterruptedException e) {e.printStackTrace();}}Overridepublic String toString() {return MyTask( name );}}public static void main(String[] args) throws InterruptedException {AtomicInteger c new AtomicInteger(1);ArrayBlockingQueueRunnable queue new ArrayBlockingQueue(2);ThreadPoolExecutor threadPool new ThreadPoolExecutor(2,3,0,TimeUnit.MILLISECONDS,queue,r - new Thread(r, myThread c.getAndIncrement()),new ThreadPoolExecutor.AbortPolicy());//下图是DiscardPolicy即直接丢弃任务。showState(queue, threadPool);threadPool.submit(new MyTask(1, 3600000));//核心线程showState(queue, threadPool);threadPool.submit(new MyTask(2, 3600000));//核心线程showState(queue, threadPool);threadPool.submit(new MyTask(3));//进入阻塞队列showState(queue, threadPool);threadPool.submit(new MyTask(4));//进入阻塞队列showState(queue, threadPool);threadPool.submit(new MyTask(5,3600000));//临时线程showState(queue, threadPool);threadPool.submit(new MyTask(6));showState(queue, threadPool);}private static void showState(ArrayBlockingQueueRunnable queue, ThreadPoolExecutor threadPool) {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}ListObject tasks new ArrayList();for (Runnable runnable : queue) {try {Field callable FutureTask.class.getDeclaredField(callable);callable.setAccessible(true);Object adapter callable.get(runnable);Class? clazz Class.forName(java.util.concurrent.Executors$RunnableAdapter);Field task clazz.getDeclaredField(task);task.setAccessible(true);Object o task.get(adapter);tasks.add(o);} catch (Exception e) {e.printStackTrace();}}LoggerUtils.main.debug(pool size: {}, queue: {}, threadPool.getPoolSize(), tasks);}}面试文稿 说一下线程池的核心参数线程池的执行原理知道嘛 在线程池中一共有7个核心参数 corePoolSize 核心线程数目 - 池中会保留的最多线程数maximumPoolSize 最大线程数目 - 核心线程救急线程的最大数目keepAliveTime 生存时间 - 救急线程的生存时间生存时间内没有新任务此线程资源会释放unit 时间单位 - 救急线程的生存时间单位如秒、毫秒等workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建救急线程执行任务threadFactory 线程工厂 - 可以定制线程对象的创建例如设置线程名字、是否是守护线程等handler 拒绝策略 - 当所有线程都在繁忙workQueue 也放满时会触发拒绝策略 拒绝策略有4种当线程数过多以后第一种是抛异常、第二种是由调用者线程执行任务、第三是丢弃当前的任务第四是丢弃阻塞队列中最早排队任务。默认是直接抛异常。 3.2 线程池中有哪些常见的阻塞队列 3 3 workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建救急线程执行任务 比较常见的有4个用的最多是ArrayBlockingQueue和LinkedBlockingQueue ArrayBlockingQueue基于数组结构的有界阻塞队列FIFO。LinkedBlockingQueue基于链表结构的有界阻塞队列FIFO。底层是单项链表。DelayedWorkQueue 是一个优先级队列它可以保证每次出队的任务都是当前队列中执行时间最靠前的。(了解SynchronousQueue不存储元素的阻塞队列每个插入操作都必须等待一个移出操作。(了解 ArrayBlockingQueue的LinkedBlockingQueue区别 ArrayBlockingQueue强制有界就是创建时必须给定容量值。LinkedBlockingQueue默认界值是Integer.MAX_VALUE所以初始最好给定一个容量的大小。往下的三点都是与其对应的数据结构有关。针对最后一点锁进行说明如下 左边是LinkedBlockingQueue加锁的方式右边是ArrayBlockingQueue加锁的方式 LinkedBlockingQueue读和写各有一把锁性能相对较好ArrayBlockingQueue只有一把锁读和写公用性能相对于LinkedBlockingQueue差一些 一般我们使用LinkedBlockingQueue面试的时候大胆说自己用的是LinkedBlockingQueue一般要自己设置一个容量值。 面试文稿 Jdk中提供了很多阻塞队列开发中常见的有两个ArrayBlockingQueue和LinkedBlockingQueue ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列它们在实现和使用上有一些关键的区别。 首先ArrayBlockingQueue是一个有界队列它在创建时必须指定容量并且这个容量不能改变。而LinkedBlockingQueue默认是无界的但也可以在创建时指定最大容量使其变为有界队列。 其次它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快因为它不需要移动其他元素来填充空间。 另外它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁一把用于控制读操作另一把用于控制写操作这样可以提高并发性能。 3.3 如何确定核心线程数 4 3 在设置核心线程数之前需要先熟悉一些执行线程池执行任务的类型 IO密集型任务 一般来说文件读写、DB读写、网络请求等 推荐核心线程数大小设置为2N1 N为计算机的CPU核数CPU密集型任务 一般来说计算型代码、Bitmap转换、Gson转换等 推荐核心线程数大小设置为N1 N为计算机的CPU核数 如何确定CPU核数呢可以java代码查看CPU核数 public class Main {public static void main(String[] args) {// 查看机器的CPU核数System.out.println(Runtime.getRuntime().availableProcessors());} } 面试文稿 根据项目的不同来确定 ① 高并发、任务执行时间短的项目 -- CPU核数1 减少线程上下文的切换 ② 并发不高、任务执行时间长的项目 IO密集型的任务 -- (CPU核数 * 2 1)计算密集型任务 -- CPU核数1 ③ 并发高、业务执行时间长解决这种类型任务的关键不在于线程池而在于整体架构的设计看看这些业务里面某些数据是否能做缓存是第一步增加服务器是第二步至于线程池的设置设置参考②JAVA开发的项目一般都是IO密集型项目很少是计算密集型的 3.4 线程池的种类有哪些 3 3 在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();}}pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-2:0 pool-1-thread-2:1 pool-1-thread-3:0 pool-1-thread-3:1//与下面一行输出比较可以看出只有三个线程 pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-2:0 pool-1-thread-2:1 单线程化的线程池它只会用唯一的工作线程来执行任务保证所有任务按照指定顺序(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();}}面试文稿 在jdk中默认提供了4中方式创建线程池 第一个是newCachedThreadPool 创建一个可缓存线程池如果线程池长度超过处理需要可灵活回收空闲线程若无可回收则新建线程。 第二个是newFixedThreadPool 创建一个定长线程池可控制线程最大并发数超出的线程会在队列 中等待。 第三个是newScheduledThreadPool 创建一个定长线程池支持定时及周期性任务执行。 第四个是newSingleThreadExecutor 创建一个单线程化的线程池它只会用唯一的工作线程来执行任 务保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 3.5 为什么不建议用Executors创建线程池 3 3 参考阿里开发手册《Java开发手册-嵩山版》 面试文稿 其实这个事情在阿里提供的最新开发手册《Java开发手册-嵩山版》中也提到了 主要原因是如果使用Executors创建线程池的话(上面图中的三个线程池它允许的请求队列默认长度是Integer.MAX_VALUE这样的话有可能导致堆积大量的请求从而导致OOM内存溢出。 所以我们一般推荐使用ThreadPoolExecutor来创建线程池这样可以明确规定线程池的参数避免资源的耗尽。 4. 多线程使用场景问题 结合真实案例来说清楚如何使用线程、如何使用线程池往往这些使用场景比较棘手需要重点掌握。 4.1 线程池使用场景CountDownLatch、Future你们项目哪里用到了多线程 3 4 juc包下的类CountDownLatch、Future。 4.1.1 CountDownLatch CountDownLatch闭锁/倒计时锁用来进行线程同步协作等待所有线程完成倒计时一个或者多个线程等待其他多个线程完成某件事情之后才能执行 其中构造参数用来初始化等待计数值await() 用来等待计数归零countDown() 用来让计数减一 public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {//初始化了一个倒计时锁 参数为 3CountDownLatch latch new CountDownLatch(3);new Thread(() - {System.out.println(Thread.currentThread().getName()-begin...);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//count--latch.countDown();System.out.println(Thread.currentThread().getName()-end... latch.getCount());}).start();new Thread(() - {System.out.println(Thread.currentThread().getName()-begin...);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}//count--latch.countDown();System.out.println(Thread.currentThread().getName()-end... latch.getCount());}).start();new Thread(() - {System.out.println(Thread.currentThread().getName()-begin...);try {Thread.sleep(1500);} catch (InterruptedException e) {throw new RuntimeException(e);}//count--latch.countDown();System.out.println(Thread.currentThread().getName()-end... latch.getCount());}).start();String name Thread.currentThread().getName();System.out.println(name -waiting...);//等待其他线程完成latch.await();System.out.println(name -wait end...);} }那么实际项目中如何使用呢下面进行说明 4.1.2 案例一es数据批量导入 在我们项目上线之前我们需要把数据库中的数据一次性的同步到es索引库中但是当时的数据好像是1000万左右一次性读取数据肯定不行oom异常当时我就想到可以使用线程池的方式导入利用CountDownLatch来控制就能避免一次性加载过多防止内存溢出 整体流程就是通过CountDownLatch线程池配合去执行 详细实现流程 把所有文章通过线程池建立多个线程实现分批导入(每页一个线程)到ES中每个线程完毕后都会调用countDown()。 4.1.3 案例二数据汇总 在一个电商网站中用户下单之后需要查询数据数据包含了三部分订单信息、包含的商品、物流信息这三块信息都在不同的微服务中进行实现的我们如何完成这个业务呢 4.1.4 案例三异步调用 很多软件都有搜索功能在进行搜索的时候需要保存用户的搜索记录而搜索记录不能影响用户的正常搜索我们通常会通过异步调用开启一个线程去执行历史记录的保存在新开启的线程在执行保历史记录的过程中可以利用线程提交搜索任务。 总结 你们项目哪里用到了多线程 批量导入:使用了线程池CountDownLatch批量把数据库中的数据导入到了ES(任意)中避免OOM。 数据汇总:调用多个接口来汇总数据如果所有接口(或部分接口)的没有依赖关系就可以使用线程池future来提升性能。 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑)可使用异步线程调用下一个方法(不需要下一级方法返回值)可以提升方法响应时间。 面试文稿 嗯~~我想一下当时的场景[根据自己简历上的模块设计多线程场景] 参考场景一 es数据批量导入 在我们项目上线之前我们需要把数据量的数据一次性的同步到es索引库中但是当时的数据好像是1000万左右一次性读取数据肯定不行oom异常如果分批执行的话耗时也太久了。所以当时我就想到可以使用线程池的方式导入利用CountDownLatchFuture来控制就能大大提升导入的时间。 参考场景二 在我做那个xx电商网站的时候里面有一个数据汇总的功能在用户下单之后需要查询订单信息也需要获得订单中的商品详细信息可能是多个还需要查看物流发货信息。因为它们三个对应的分别三个微服务如果一个一个的操作的话互相等待的时间比较长。所以我当时就想到可以使用线程池让多个线程同时处理最终再汇总结果就可以了当然里面需要用到Future来获取每个线程执行之后的结果才行 参考场景三 《黑马头条》项目中使用的 我当时做了一个文章搜索的功能用户输入关键字要搜索文章同时需要保存用户的搜索记录搜索历史这块我设计的时候为了不影响用户的正常搜索我们采用的异步的方式进行保存的为了提升性能我们加入了线程池也就说在调用异步方法的时候直接从线程池中获取线程使用 4.2 如何控制某个方法允许并发访问线程的数量 3 2 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();}}}Semaphore使用步骤 创建Semaphore对象可以给一个容量semaphore.acquire():请求一个信号量这时候的信号量个数-1(一旦没有可使用的信号量也即信号量个数变为负数时再次请求的时候就会阻塞直到其他线程释放了信号量)semaphore.release():释放一个信号量此时信号量个数1 面试文稿 如何控制某个方法允许并发访问线程的数量 在多线程中利用JDK提供的一个工具类Semaphore信号量。在并发的情况下可以控制方法的访问量 创建Semaphore对象可以给一个容量acquire()可以请求一个信号量这时候的信号量个数-1release()释放一个信号量此时信号量个数1 5. 其他 5.1 谈谈你对ThreadLocal的理解 3 4 5.1.1 概述 ThreadLocal是多线程中对于解决线程安全的一个操作类它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。 案例使用JDBC操作数据库时会将每一个线程的Connection放入各自的ThreadLocal中从而保证每个线程都在各自的 Connection 上进行数据库的操作避免A线程关闭了B线程的连接。 5.1.2 ThreadLocal基本使用 三个主要方法 set(value) 设置值get() 获取值remove() 清除值 public class ThreadLocalTest {static ThreadLocalString threadLocal new ThreadLocal();public static void main(String[] args) {new Thread(() - {String name Thread.currentThread().getName();threadLocal.set(itcast);print(name);System.out.println(name -after remove : threadLocal.get());}, t1).start();new Thread(() - {String name Thread.currentThread().getName();threadLocal.set(itheima);print(name);System.out.println(name -after remove : threadLocal.get());}, t2).start();}static void print(String str) {//打印当前线程中本地内存中本地变量的值System.out.println(str : threadLocal.get());//清除本地内存中的本地变量threadLocal.remove();}}5.1.3 ThreadLocal的实现原理源码解析面试官比较关心 ThreadLocal本质来说就是一个线程内部存储类从而让多个线程只操作自己内部的值从而实现线程数据隔离。 在ThreadLocal中有一个内部类叫做ThreadLocalMap类似于HashMap ThreadLocalMap中有一个属性table数组这个是真正存储数据的位置 set方法 get方法/remove方法 后面会继续追问你知道ThreadLocal的内存泄漏问题吗 5.1.4 ThreadLocal-内存泄露问题 Java对象中的四种引用类型强引用、软引用、弱引用、虚引用 强引用最为普通的引用方式表示一个对象处于有用且必须的状态如果一个对象具有强引用则GC并不会回收它。即便堆中内存不足了宁可出现OOM也不会对其进行回收 弱引用表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时一旦发现弱引用就会回收到弱引用相关联的对象。对于弱引用的回收无关内存区域是否足够一旦发现则会被回收 每一个Thread维护一个ThreadLocalMap在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例value为线程变量的副本。 由于强引用不会被回收日积月累就会导致内存泄露解决方法在使用ThreadLocal的时候强烈建议务必手动remove。 面试文稿 面试官谈谈你对ThreadLocal的理解 候选人嗯是这样的~ ThreadLocal 主要功能有两个第一个是可以实现资源对象的线程隔离让每个线程各用各的资源对象避免争用引发的线程安全问题第二个是实现了线程内的资源共享 面试官好的那你知道ThreadLocal的底层原理实现吗 候选人嗯知道一些~ 在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量用来存储资源对象 当我们调用 set 方法就是以 ThreadLocal 自己作为 key资源对象作为 value放入当前线程的 ThreadLocalMap 集合中 当调用 get 方法就是以 ThreadLocal 自己作为 key到当前线程中查找关联的资源值 当调用 remove 方法就是以 ThreadLocal 自己作为 key移除当前线程关联的资源值 面试官好的那关于ThreadLocal会导致内存溢出这个事情了解吗 候选人嗯我之前看过源码我想一下~~ 是因为ThreadLocalMap 中的 key 被设计为弱引用它是被动的被GC调用释放key不过关键点是只有key可以得到内存释放而value不会因为value是一个强引用。 在使用ThreadLocal 时都把它作为静态变量即强引用因此无法被动依靠 GC 回收建议主动的remove 释放 key这样就能避免内存溢出。
http://www.w-s-a.com/news/597421/

相关文章:

  • 做做网站已更新878网站正在建设中
  • dz旅游网站模板网站上做百度广告赚钱么
  • 青岛外贸假发网站建设seo优化名词解释
  • 四川建设厅网站施工员证查询网站建设行业政策
  • 网站全站出售dw怎么设计网页
  • 合肥网站建设方案服务网站建设推荐郑国华
  • 襄阳网站建设需要多少钱台州网站设计公司网站
  • 东莞专业拍摄做网站照片如何在百度上发布自己的广告
  • 网站建设费 科目做网站建设最好学什么
  • php商城网站建设多少钱深圳市建设
  • 有什么做糕点的视频网站黄岛做网站
  • 做视频课程网站建设一个普通网站需要多少钱
  • 专做化妆品的网站合肥做网站建设公司
  • 唐山企业网站网站建设费计入那个科目
  • 企业网站制作运营彩虹云主机官网
  • 如何建设废品网站如何在阿里云云服务器上搭建网站
  • 如何建立网站后台程序wordpress 后台管理
  • 山东外贸网站建设怎么样wordpress首页左图右文
  • 志丹网站建设wordpress 形式修改
  • 南通seo网站推广费用网站建设就业前景
  • 自适应网站做mip改造浏览器广告投放
  • 网站meta网页描述网站的推广费用
  • 偃师市住房和城乡建设局网站网站个人主页怎么做
  • 做网站要实名认证吗wordpress去掉仪表盘
  • 在哪做网站好Python建网站的步骤
  • 卢松松的网站办公室设计布局
  • 住房城乡建设干部学院网站织梦网站0day漏洞
  • 企业网站seo优帮云手机桌面布局设计软件
  • 无证做音频网站违法吗智能建站加盟电话
  • 鹿泉专业网站建设做网站为什么要建站点