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

cms网站后台管理系统建设一个网站流程图

cms网站后台管理系统,建设一个网站流程图,做一款小程序需要多少钱,西平县住房城乡建设局网站写在前面 ⭐️在无数次的复习巩固中#xff0c;我逐渐意识到一个问题#xff1a;面对同样的面试题目#xff0c;不同的资料来源往往给出了五花八门的解释#xff0c;这不仅增加了学习的难度#xff0c;还容易导致概念上的混淆。特别是当这些信息来自不同博主的文章或是视…写在前面 ⭐️在无数次的复习巩固中我逐渐意识到一个问题面对同样的面试题目不同的资料来源往往给出了五花八门的解释这不仅增加了学习的难度还容易导致概念上的混淆。特别是当这些信息来自不同博主的文章或是视频教程时它们之间可能存在的差异性使得原本清晰的概念变得模糊不清。更糟糕的是许多总结性的面试经验谈要么过于繁复难以记忆要么就是过于简略对关键知识点一带而过常常在提及某项技术时又引出了更多未经解释的相关术语和实例例如在讨论ReentrantLock时经常会提到这是一个可重入锁并存在公平与非公平两种实现方式但对于这两种锁机制背后的原理以及使用场景往往语焉不详。 ⭐️正是基于这样的困扰与思考我决定亲自上阵撰写一份与众不同的面试指南。这份指南不仅仅是对现有资源的简单汇总更重要的是它融入了我的个人理解和解读。我力求回归技术书籍本身以一种层层递进的方式剖析复杂的技术概念让那些看似枯燥乏味的知识点变得生动起来并在我的脑海中构建起一套完整的知识体系。我希望通过这种方式不仅能帮助自己在未来的技术面试中更加从容不迫也能为同行们提供一份有价值的参考资料使大家都能在这个过程中有所收获。 Java多线程相关面试题 1 线程的基础知识 面试官聊一下并行和并发有什么区别 候选人 现在都是多核CPU在多核CPU下。 并发是指在同一时间段内系统有能力处理多个事件多个线程轮流使用一个或多个CPU 并行是指在同一时刻多个任务可以在多个处理器核心上同时执行4核CPU同时执行4个线程 面试官说一下线程和进程的区别 候选人 进程是正在运行程序的实例进程中包含了线程每个线程执行不同的任务 不同的进程使用不同的内存空间在当前进程下的所有线程可以共享进程的堆和⽅法区资源但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈 线程更轻量线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程) 下⾯来思考这样⼀个问题为什么程序计数器、虚拟机栈和本地⽅法栈是线程私有的呢 在多线程的情况下程序计数器⽤于记录当前线程执⾏的位置从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。如果执⾏的是 native ⽅法那么程序计数器记录的是 undefined 地址只有执⾏的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。所以程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置。虚拟机栈为虚拟机执⾏ Java⽅法 也就是字节码服务⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。所以为了保证线程中的局部变量不被别的线程访问到虚拟机栈和本地⽅法栈是线程私有的。 面试官什么是上下⽂切换? 候选人源自《Java并发编程艺术》1.1节 CPU通过时间片分配算法来循环执行任务当前任务执行一个时间后片后会切换到下一个任务。但是在切换前会保存上一个任务的状态以便下次切换回这个任务时可以再加载这个任务的状态。所以任务从保存到再加载的过程就是⼀次上下⽂切换。 这就像我们同时读两本书当我们在读一本英文的技术书时发现某个单词不认识于是便打开中英文字典但是在放下英文技术书之前大脑必须先记住这本书读到了多少页的第多少行等查完单词之后能够继续读这本书。这样的切换是会影响读书效率的同样上下文切换也会影响多线程的执行速度。 面试官如何减少上下⽂切换? 候选人源自《Java并发编程艺术》1.1.3节 减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 无锁并发编程。多线程竞争锁时会引起上下文切换所以多线程处理数据时可以用一些办法来避免使用锁如将数据的ID 按照 Hash 算法取模分段不同的线程处理不同段的数据。 CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据而不需要加锁。 使用最少线程。避免创建不需要的线程比如任务很少但是创建了很多线程来处理这样会造成大量线程都处于等待状态。 协程在单线程里实现多任务的调度并在单线程里维持多个任务间的切换。 面试官如果在java中创建线程有哪些方式 候选人 在java中一共有四种常见的创建方式分别是继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。通常情况下我们项目中都会采用线程池的方式创建线程。 面试官好的刚才你说的runnable 和 callable 两个接口创建线程有什么不同呢 候选人 Runnable 接口run方法无返回值Callable接口call方法有返回值是个泛型和Future、FutureTask配合可以用来获取异步执行的结果 还有一个就是他们异常处理也不一样。Runnable接口run方法只能抛出运行时异常也无法捕获处理Callable接口call方法允许抛出异常可以获取异常信息 在实际开发中如果需要拿到执行的结果需要使用Callalbe接口创建线程调用FutureTask.get()得到可以得到返回值此方法会阻塞主进程的继续往下执行如果不调用不会阻塞。 面试官线程包括哪些状态状态之间是如何变化的 候选人 在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是初始、运行、终止、阻塞、等待和超时等待六种。图源《Java 并发编程艺术》4.1.4 节 当一个线程对象被创建但还未调用 start 方法时处于初始状态调用了 start 方法就会由初始进入运行状态。如果线程内代码已经执行完毕由运行进入终止状态。当然这些是一个线程正常执行情况。 如果线程获取锁失败后由运行进入 Monitor 的阻塞队列阻塞只有当持锁线程释放锁时会按照一定规则唤醒阻塞队列中的阻塞线程唤醒后的线程进入运行状态。 如果线程获取锁成功后但由于条件不满足调用了 wait() 方法此时从运行状态释放锁等待状态当其它持锁线程调用 notify() 或 notifyAll() 方法会恢复为运行状态。 还有一种情况是调用 sleep(long) 方法也会从运行状态进入超时等待状态不需要主动唤醒超时时间到自然恢复为运行状态。 图源《Java 并发编程艺术》4.1.4 节 面试官说说sleep()⽅法和wait()⽅法区别和共同点? 候选人 它们两个的相同点是都可以让当前线程暂时放弃 CPU 的使用权进入阻塞状态。 不同点主要有三个方面 第一方法归属不同 sleep(long) 是 Thread 的静态方法。而 wait()是 Object 的成员方法每个对象都有 第二线程醒来时机不同 线程执行 sleep(long) 会在等待相应毫秒后醒来而 wait() 需要被 notify 唤醒wait() 如果不唤醒就一直等下去 第三锁特性不同 wait 方法的调用必须先获取 wait 对象的锁而 sleep 则无此限制 wait 方法执行后会释放对象锁允许其它线程获得该对象锁相当于我放弃 cpu但你们还可以用 sleep 如果在 synchronized 代码块中执行并不会释放对象锁相当于我放弃 cpu你们也用不了 面试官好的我现在举一个场景你来分析一下怎么做新建 T1、T2、T3 三个线程如何保证它们按顺序执行 候选人 可以这么做在多线程中有多种方法让线程按特定顺序执行可以用线程类的join()方法在一个线程中启动另一个线程另外一个线程完成该线程继续执行。 比如说 使用join方法T3调用T2T2调用T1这样就能确保T1就会先完成而T3最后完成。 import java.util.concurrent.TimeUnit;public class Join {public static void main(String[] args) throws Exception {Thread previous Thread.currentThread();for (int i 0; i 3; i) {//每个线程拥有前一个线程的引用需要等待前一个线程终止才能从等待中返回Thread thread new Thread(new Domino(previous), String.valueOf(i));thread.start();previous thread;}TimeUnit.SECONDS.sleep(5);System.out.println(Thread.currentThread().getName() terminate.);}static class Domino implements Runnable {private Thread thread;public Domino(Thread thread) {this.thread thread;}public void run() {try {thread.join();} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() terminate.);}} }输出如下。 main terminate. 0 terminate. 1 terminate. 2 terminate. 面试官在我们使用线程的过程中有两个方法。线程的 run()和 start()有什么区别 候选人 start方法用来启动线程通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。run方法封装了要被线程执行的代码可以被调用多次。 总结调⽤start() ⽅法⽅可启动线程并使线程进⼊就绪状态直接执⾏run()⽅法的话不会以多线程的⽅式执⾏。 面试官那如何停止一个正在运行的线程呢 候选人有三种方式可以停止线程 第一可以使用退出标志使线程正常退出也就是当run方法完成后线程终止一般我们加一个标记 第二可以使用线程的suspend()、resume()和stop()方法强行终止不过一般不推荐这个方法已作废 第三可以使用线程的interrupt方法中断线程内部其实也是使用中断标志来中断线程 我们项目中使用的话建议使用第一种或第三种方式中断线程。 《Java并发编程的艺术》4.2.4节: 不建议使用suspend()、resume()和stop()方法的原因主要有以suspend()方法为例在调用后线程不会释放已经占有的资源(比如锁)而是占有着资源进入睡眠状态这样容易引发死锁问题。同样stop()方法在终结一个线程时不会保证线程的资源正常释放通常是没有给予线程完成资源释放工作的机会因此会导致程序可能工作在不确定状态下。 2 线程中并发锁 面试官说一下悲观锁与乐观锁的区别 候选人 悲观锁和乐观锁是数据库管理系统中用于处理并发事务的不同方式。 悲观锁 基于假设当事务要对某个数据进行操作时认为很可能会发生冲突。因此在事务开始时就对数据进行锁定阻止其他事务同时对其进行修改直到当前事务完成。这种方式虽然能够保证数据的一致性和准确性但由于锁的存在可能会导致其他事务等待增加了系统开销。 乐观锁 则基于另一种假设认为事务之间发生冲突的概率较低因此在读取数据时不立即加锁而是允许多个事务同时读取数据。当事务尝试提交更改时会检查在此期间是否有其他事务修改过相同的数据。如果有冲突则当前事务失败可能需要回滚并重新开始。乐观锁通常通过版本号或时间戳来检测数据是否已被修改。 简单来说悲观锁适合于数据冲突频繁的场景它通过加锁来防止冲突而乐观锁更适合于读多写少的场景通过在提交时检查冲突来减少锁的竞争。 面试官说一下公平锁与非公平锁的区别 候选人 公平锁Fair Lock 按照线程在队列中的排队顺序先到者先拿到锁。 非公平锁Unfair Lock 当线程要获取锁时无视队列顺序直接去抢锁谁抢到就是谁的。 《Java并发编程的艺术》5.3节 这里提到一个锁获取的公平性问题如果在绝对时间上先对锁进行获取的请求一定先被满足那么这个锁是公平的反之是不公平的。公平的获取锁也就是等待时间最长的线程最优先获取锁也可以说锁获取是顺序的。 事实上公平的锁机制往往没有非公平的效率高但是并不是任何场景都是以 TPS 作为唯一的指标公平锁能够减少“饥饿” 发生的概率等待越久的请求越是能够得到优先满足。 在测试中公平性锁与非公平性锁相比总耗时是其 94.3倍总切换次数是其 133 倍。可以看出公平性锁保证了锁的获取按照 FIFO 原则而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”但极少的线程切换保证了其更大的吞吐量。 面试官讲一下synchronized关键字的底层原理 候选人synchronized关键字解决的是多个线程之间访问资源的同步性synchronized 关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。 synchronized 底层使用的JVM级别中的Monitor监视器锁 来决定当前线程是否获得了锁如果某一个线程获得了锁在没有释放锁之前其他线程是不能或得到锁的。synchronized 属于悲观锁。 在 Java 早期版本中 synchronized 属于 重量级锁效率低下。 《Java并发编程的艺术》2.2节: 从JVM 规范中可以看到 Synchonized在JVM里的实现原理JVM基于进入和退出Monitor对象来实现方法同步和代码块同步但两者的实现细节不一样。 代码块同步是使用 monitorenter 和 monitorexit 指令实现的monitorenter 指令是在编译后插入到同步代码块的开始位置而 monitorexit 是插入到方法结束处和异常处JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。方法同步是使用另外一种方式实现的JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法从⽽执⾏相应的同步调⽤。 不过两者的本质都是对 对象监视器 monitor 的获取。 面试官你能具体说下Monitor 吗 候选人monitor对象存在于每个Java对象的对象头中synchronized 锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因。 monitor内部维护了三个变量: WaitSet保存处于Waiting状态的线程 EntryList保存处于Blocked状态的线程 Owner持有锁的线程 只有一个线程获取到的标志就是在monitor中设置成功了Owner一个monitor中只能有一个Owner。 在上锁的过程中如果有其他线程也来抢锁则进入EntryList 进行阻塞当获得锁的线程执行完了释放了锁就会唤醒EntryList 中等待的线程竞争锁竞争的时候是非公平的。 面试官说说⾃⼰是怎么使⽤ synchronized 关键字的 候选人 1.修饰实例⽅法: 作⽤于当前对象实例加锁进⼊同步代码前要获得 当前对象实例的锁 synchronized void method() {//业务代码 }2.修饰静态⽅法 也就是给当前类加锁会作⽤于类的所有对象实例 进⼊同步代码前要获得 当前class的锁。因为静态成员不属于任何⼀个实例对象是类成员 static 表明这是该类的⼀个静态资源不管 new 了多少个对象只有⼀份。所以如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法是允许的不会发⽣互斥现象因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。 synchronized void staic method() {//业务代码 }3.修饰代码块 指定加锁对象对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized( .class) 表示进⼊同步代码前要获得 当前 class 的锁 synchronized(this) {//业务代码 }总结 synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class类上锁。 synchronized 关键字加到实例⽅法上是给对象实例上锁。 尽量不要使⽤ synchronized(String a) 因为 JVM 中字符串常量池具有缓存功能 面试官单例模式了解吗来给我⼿写⼀下给我解释⼀下双重检验锁⽅式实现单例模式的原理呗 候选人双重校验锁实现对象单例线程安全 这个必须要会 public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getInstance() {//先判断对象是否已经实例过没有实例化过才进⼊加锁代码if (uniqueInstance null) {//类对象加锁synchronized (Singleton.class) {if (uniqueInstance null) {uniqueInstance new Singleton(); }}}return uniqueInstance;}}另外uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的 uniqueInstance new Singleton(); 这段代码其实是分为三步执⾏ 为 uniqueInstance 分配内存空间初始化 uniqueInstance将 uniqueInstance 指向分配的内存地址 如果没有volatile由于 JVM 具有重排序的特性执⾏顺序有可能变成 1-3-2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如线程 T1 执⾏了 1 和 3此时 T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空因此返回uniqueInstance 但此时 uniqueInstance 还未被初始化。所以这是一种错误的用法 但是如果使⽤ volatile 就可以禁⽌ JVM 的指令重排实现线程安全的延迟初始化保证在多线程环境下也能正常运⾏。 面试官关于synchronized 的锁升级的情况了解吗 候选人Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。 重量级锁底层使用的Monitor实现里面涉及到了用户态和内核态的切换、进程的上下文切换成本较高性能比较低。 轻量级锁线程加锁的时间是错开的也就是没有竞争可以使用轻量级锁来优化。轻量级锁修改对象头的锁标志相对重量级锁性能提升很多。每次修改都是CAS操作保证原子性。 偏向锁一段很长的时间内都只被一个线程使用锁可以使用了偏向锁在第一次获得锁时会有一个CAS操作之后该线程再获取锁只需要判断mark word中是否是自己的线程id即可而不是开销相对较大的CAS命令。图源《Java并发编程的艺术》2.2.1节 《Java并发编程的艺术》2.2.1节 在 Java SE 1.6中锁一共有4种状态级别从低到高依次是:无锁状态、偏向锁状态轻量级锁状态和重量级锁状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略目的是为了提高获得锁和释放锁的效率。 面试官好的刚才你说了synchronized它在高并发量的情况下性能不高在项目该如何控制使用锁呢 候选人在高并发下我们可以采用ReentrantLock来加锁。 面试官说下ReentrantLock的使用方式和底层原理 候选人 ReentrantLock是一个可重入锁 调用 Lock 方法获取了锁之后再次调用 Lock是不会再阻塞内部直接增加重入次数标识这个线程已经重复获取一把锁而不需要等待锁的释放。 ReentrantLock是属于JUC包JUC是Java平台提供的一个用于支持高并发程序设计的工具包下的类属于api层面的锁跟synchronized一样都是悲观锁。通过lock()用来获取锁unlock()释放锁。 它的底层实现原理主要利用CASAQS队列来实现。它支持公平锁和非公平锁。构造方法接受一个可选的公平参数默认非公平锁当设置为true时表示公平锁否则为非公平锁。 面试官刚才你说了CAS和AQS你能介绍一下吗 候选人 CAS的全称是 Compare And Swap(比较再交换)它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。 《Java并发编程的艺术》2.3节对CAS操作的解释 CAS 操作需要输入两个数值一个旧值(期望操作前的值)和一个新值在操作期间先比较旧值有没有发生变化如果没有发生变化才交换成新值发生了变化则不交换。 CAS使用到的地方很多AQS框架、AtomicXXX类 在操作共享变量的时候使用的自旋锁效率上更高一些 CAS的底层是调用的Unsafe类中的方法都是操作系统提供的其他语言实现 AQS的全称是AbstractQueuedSynchronizer是阻塞式锁和相关的同步器工具的框架。使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器⽐如 ReentrantLock Semaphore 其他的诸如ReentrantReadWriteLock CountDownLatch FutureTask 等等皆是基于 AQS 的。当然我们⾃⼰也能利⽤ AQS ⾮常轻松容易地构造出符合我们⾃⼰需求的同步器。 内部有一个属性 state 属性来表示资源的状态默认state等于0表示没有获取锁state等于1的时候才标明获取到了锁。通过CAS机制设置 state 状态。 在它的内部还提供了基于 FIFO 的等待队列CLH 队列是一个双向列表其中 tail 指向队列最后一个元素 head 指向队列中最久的一个元素 面试官AQS对资源的共享方式Semaphore、CountDownLatch、CyclicBarrier 候选人 Exclusive独占只有⼀个线程能执⾏如 ReentrantLock 可分为公平锁和⾮公平锁。 Share共享多个线程可同时执⾏如CountDownLatch倒计时器、Semaphore信号量、CyclicBarrier循环栅栏 。 ReentrantReadWriteLock 可以看成是组合式因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某⼀资源进⾏读。 不同的⾃定义同步器争⽤共享资源的⽅式也不同。⾃定义同步器在实现时只需要实现共享资源state 的获取与释放⽅式即可⾄于具体线程等待队列的维护如获取资源失败⼊队/唤醒出队等AQS 已经在顶层实现好了。 《Java并发编程的艺术》 8.1 8.2 8.3节 Semaphore信号量是用来控制同时访问特定资源的线程数量大白话可以指定多个线程同时访问某个资源它通过协调各线程以保证合理的使用公共资源。Semaphore可以用于做流量控制特别是公用资源有限的应用场景比如数据库连接。Semaphore 的用法也很简单首先线程使用 Semaphore 的 acquire( ) 方法获取一个许可证使用完之后调用 release( ) 方法归还许可证。还可以用 tryAcquire( )方法尝试获取许可证。CountDownLatch倒计时器允许一个或多个线程等待其他线程完成操作。大白话它可以让某一个线程等待直到倒计时结束再开始执行。CountDownLatch 的构造函数接收一个 int 类型的参数作为计数器如果你想等待 N个点完成这里就传入N。当我们调用CountDownLatch的countDown方法时N就会减1CountDownLatch 的await方法会阻塞当前线程直到N变成零。CyclicBarrier 的字⾯意思是可循环使⽤ Cyclic 的屏障 Barrier 。它要做的事情是让⼀组线程到达⼀个屏障也可以叫同步点时被阻塞直到最后⼀个线程到达屏障时屏障才会开⻔所有被屏障拦截的线程才会继续运行。 CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) 其参数表示屏障拦截的线程数量每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障然后当前线程被阻塞。 // CountDownLatch的用法 import java.util.concurrent.CountDownLatch;public class CountDownLatchTest {static CountDownLatch c new CountDownLatch(2);public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {Overridepublic void run() {System.out.println(1);c.countDown();System.out.println(2);c.countDown();}}).start();c.await();System.out.println(3);}}// CyclicBarrier的用法 // 因为主线程和子线程的调度是由CPU决定的两个线程都有可能先执行所以会出现两种输出 // 第一种 1 2 第二种 2 1 import java.util.concurrent.CyclicBarrier;public class CyclicBarrierTest {// 如果把 new CyclicBarrier(2)修改成 new CyclicBarrier(3)则主线程和子线程会永远等待。// 因为没有第三个线程执行 await 方法即没有第三个线程到达屏障所以之前到达屏障的两个线程都不会继续执行。static CyclicBarrier c new CyclicBarrier(2);public static void main(String[] args) {new Thread(new Runnable() {Overridepublic void run() {try {c.await();} catch (Exception e) {}System.out.println(1);}}).start();try {c.await();} catch (Exception e) {}System.out.println(2);} }面试官CyclicBarrier和CountDownLatch的区别 候选人源自《Java并发编程的艺术》 8.2.3节 CountDownLatch 的计数器只能使用一次而 CyclicBarrier 的计数器可以使用 reset) 方法重置。所以CyclicBarrier 能处理更为复杂的业务场景。例如如果计算发生错误可以重置计数器并让线程重新执行一次。CyclicBarrier 还提供其他有用的方法比如 getNumberWaiting方法可以获得 CyclicBarrier阻塞的线程数量。isBroken方法用来了解阻塞的线程是否被中断。 面试官synchronized和Lock有什么区别 ? 候选人 第一语法层面 synchronized 是关键字源码在 jvm 中用 c 语言实现退出同步代码块锁会自动释放Lock 是接口源码由 jdk 提供用 java 语言实现需要手动调用 unlock 方法释放锁 第二功能层面 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能例如获取等待状态、公平锁、可打断、可超时、多条件变量同时Lock 可以实现不同的场景如 ReentrantLock ReentrantReadWriteLock 第三性能层面 在没有竞争时synchronized 做了很多优化如偏向锁、轻量级锁性能不赖在竞争激烈时Lock 的实现通常会提供更好的性能 统合来看需要根据不同的场景来选择不同的锁的使用。 面试官说说synchronized关键字和volatile关键字的区别 候选人synchronized 关键字和 volatile 关键字是两个互补的存在⽽不是对⽴的存在 volatile 关键字是线程同步的轻量级实现所以 volatile 性能肯定⽐ synchronized关键字要好。但是 volatile 关键字只能⽤于变量⽽ synchronized 关键字可以修饰⽅法以及代码块。 volatile 关键字能保证数据的可⻅性但不能保证数据的原⼦性。 synchronized 关键字两者都能保证。 《Java并发编程的艺术》 3.4.1节 简而言之volatile 变量自身具有下列特性。 可见性。对一个 volatile 变量的读总是能看到(任意线程)对这个 volatile 变量最后的写入。 原子性。对任意单个 volatile 变量的读/写具有原子性但类似于 volatile 这种复合操作不具有原子性。大白话volatile 无法保证原子性 ,只能保证自身读写为原子操作 volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性。 面试官说说Synchronized和ReentrantLock的区别 候选人 Synchronized 可以用来修饰普通方法、静态方法和代码块ReentrantLock 只能用在代码块上。Synchronized 会自动加锁和释放锁ReentrantLock需手动加锁和释放锁。Synchronized 属于非公平锁ReentrantLock 既可以是公平锁也可以是非公平锁。Synchronized 是JVM通过 monitor 实现的ReentrantLock是通过CASAQS队列实现的。 面试官死锁产生的条件是什么 候选人一个线程需要同时获取多把锁这时就容易发生死锁。 死锁必须具备以下四个条件 互斥条件该资源任意⼀个时刻只由⼀个线程占⽤。 请求与保持条件⼀个进程因请求资源⽽阻塞时对已获得的资源保持不放。 不剥夺条件 线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺只有⾃⼰使⽤完毕后才释放资源。 循环等待条件若⼲进程之间形成⼀种头尾相接的循环等待资源关系。 面试官如何避免线程死锁? 候选人 我上⾯说了产⽣死锁的四个必要条件为了避免死锁我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下 破坏互斥条件 这个条件我们没有办法破坏因为我们⽤锁本来就是想让他们互斥的临界资源需要互斥访问。 破坏请求与保持条件 ⼀次性申请所有的资源。 破坏不剥夺条件 占⽤部分资源的线程进⼀步申请其他资源时如果申请不到可以主动释放它占有的资源。 破坏循环等待条件 靠按序申请资源来预防。按某⼀顺序申请资源释放资源则反序释放。破坏循环等待条件。 《Java并发编程的艺术》 1.2节 现在我们介绍避免死锁的几个常见方法。 避免一个线程同时获取多个锁。避免一个线程在锁内同时占用多个资源尽量保证每个锁只占用一个资源。尝试使用定时锁使用 lock.tryLock(timeout)来替代使用内部锁机制。对于数据库锁加锁和解锁必须在一个数据库连接里否则会出现解锁失败的情况。 面试官如何进行死锁诊断 候选人这个也很容易我们只需要通过jdk自动的工具就能搞定。 我们可以先通过jps来查看当前java程序运行的进程id然后通过jstack来查看这个进程id就能展示出来死锁的问题并且可以定位代码的具体行号范围我们再去找到对应的代码进行排查就行了。 拓展 jps输出JVM中运行的进程状态信息。 jstack查看java进程内线程的堆栈信息。 面试官请谈谈你对 volatile 的理解 候选人volatile 是一个关键字可以修饰类的成员变量、类的静态成员变量主要有两个功能 第一保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。 第二 禁止进行指令重排序可以保证代码执行有序性。底层实现原理是添加了一个内存屏障通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 《Java并发编程的艺术》 3.2节 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 面试官那你能聊一下ConcurrentHashMap的原理吗 《Java并发编程的艺术》 6.1.1 为什么要使用 ConcurrentHashMap? 在并发编程中使用 HashMap 可能导致程序死循环。而使用线程安全的 HashTable 效率又非常低下基于以上两个原因便有了ConcurrentHashMap 的登场机会。 1线程不安全的HashMap 在多线程环境下使用 HashMap 进行 put 操作会引起死循环导致 CPU 利用率接近100%所以在并发情况下不能使用 HashMap。HashMap 在并发执行 put 操作时会引起死循环是因为多线程会导致 HashMap 的 Entry 链表形成环形数据结构一旦形成环形数据结构Entry的next节点永远不为空就会产生死循环获取 Entry。 2效率低下的HashTable HashTable 容器使用synchronized来保证线程安全但在线程竞争激烈的情况下HashTable 的效率非常低下。因为当一个线程访问HashTable 的同步方法其他线程也访问HashTable 的同步方法时会进入阻塞或轮询状态。如线程1使用 put 进行元素添加线程2 不但不能使用 put 方法添加元素也不能使用 get 方法来获取元素所以竞争越激烈效率越低。 3ConcurrentHashMap 的锁分段技术可有效提升并发访问率 HashTable 容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问 HashTable的线程都必须竞争同一把锁假如容器里有多把锁每一把锁用于锁容器其中一部分数据那么当多线程访问容器里不同数据段的数据时线程间就不会存在锁竞争从而可以有效提高并发访问效率这就是ConcurrentHashMap 所使用的锁分段技术。 候选人ConcurrentHashMap 是一种线程安全的高效Map集合jdk1.7和1.8也做了很多调整。 JDK1.7的底层采用是分段的数组链表 实现JDK1.8 采用的数据结构跟HashMap1.8的结构一样数组链表/红黑二叉树。 在jdk1.7中ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成。Segment 是一种可重入锁(ReentrantLock)扮演锁的⻆⾊。HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个Segment数组。Segment 的结构和 HashMap 类似是一种数组和链表结构。一个Segment 里包含一个 HashEntry 数组每个 HashEntry 是一个链表结构的元素每个 Segment 守护着一个 HashEntry 数组当对 HashEntry 数组的数据进行修改时必须首先获得与它对应的Segment锁。 在jdk1.8中ConcurrentHashMap 取消了 Segment 分段锁采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap的结构类似数组链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值8时将链表寻址时间复杂度为O(N)转换为红⿊树寻址时间复杂度为 O(log(N))。synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点这样只要 hash 不冲突就不会产⽣并发效率⼜提升 N 倍。 面试官说说ConcurrentHashMap的get、put、size操作 候选人源自《Java并发编程的艺术》 6.1.5节 get操作 先经过一次散列然后使用这个散列值通过散列运算定位到 Segment再通过散列算法定位到元素代码如下。 public v get(object key) {int hash hash(key.hashcode());return segmentFor(hash).get(key, hash); }get 操作的高效之处在于整个 get 过程不需要加锁除非读到的值是空才会加锁重读。我们知道 HashTable 容器的 get 方法是需要加锁的那么 ConcurrentHashMap 的 get 操作是如何做到不加锁的呢?原因是它的 get方法里将要使用的共享变量都定义成 volatile 类型。定义成 volatile 的变量能够在线程之间保持可见性能够被多线程同时读并且保证不会读到过期的值但是只能被单线程写(有一种情况可以被多线程写就是写入的值不依赖于原值)在get操作里只需要读不需要写共享变量 count 和 value所以可以不用加锁。之所以不会读到过期的值是因为根据 Java 内存模型的 happens-before 原则对 volatile 字段的写入操作先于读操作即使两个线程同时修改和获取 volatile 变量get操作也能拿到最新的值这是用 volatile替换锁的经典应用场景。 put操作 由于 put 方法里需要对共享变量进行写入操作所以为了线程安全在操作共享变量时必须加锁。put 方法首先定位到 Segment然后在 Segment 里进行插入操作。插入操作需要经历两个步骤第一步断是否需要对 Segment 里的 HashEntry 数组进行扩容第二步定位添加元素的位置然后将其放在 HashEnty 数组里。 1是否需要扩容 在插入元素前会先判断Segment 里的 HashEnty数组是否超过容量(threshold)如果超过阈值则对数组进行扩容。值得一提的是Segment的扩容判断比 HashMap 更恰当因为 HashMap 是在插入元素后判断元素是否已经到达容量的如果到达了就进行扩容可能扩容之后没有新元素插入这时 HashMap 就进行了一次无效的扩容。 2如何扩容 在扩容的时候首先会创建一个容量是原来容量两倍的数组然后将原数组里的元素进行再散列后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容而只对某个 Segment 进行扩容。 size 操作 如果要统计整个 ConcurrentHashMap 里元素的大小就必须统计所有 Segment 里元素的大小后求和。Segment里的全局变量 count 是一个 volatile 变量那么在多线程场景下是不是直接把所有 Segment 的 count 相加就可以得到整个 ConcurrentHashMap 大小了呢? 不是的虽然相加时可以获取每个 Segment 的 count 的最新值但是可能累加前使用的 count 发生了变化那么统计结果就不准了。所以最安全的做法是在统计size的时候把所有Segment 的put、remove 和 clean 方法全部锁住但是这种做法显然非常低效。 因为在累加 count 操作过程中之前累加过的count发生变化的几率非常小所以ConcurrentHashMap 的做法是尝试2次通过不锁住Segment的方式来统计各个Segment大小如果统计的过程中容器的count发生了变化则再采用加锁的方式来统计所有Segment 的大小。 那么 ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount 变量在 put、remove 和 clean 方法里操作元素前都会将变量 modCount 进行加1.那么在统计 size 前后比较 modCount 是否发生变化从而得知容器的大小是否发生变化。 3 线程池 面试官 为什么要⽤线程池 候选人源自《Java 并发编程的艺术》 9.1节 在开发过程中合理地使用线程池能够带来3个好处 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。 提⾼响应速度。当任务到达时任务可以不需要的等到线程创建就能⽴即执⾏。 提⾼线程的可管理性。线程是稀缺资源如果⽆限制的创建不仅会消耗系统资源还会降低系统的稳定性使⽤线程池可以进⾏统⼀的分配调优和监控。 面试官线程池四种创建方式 候选人 在jdk中默认提供了4种方式创建线程池。 第一个是newCachedThreadPool 创建可以缓存的线程池有任务提交到线程池时如果有空闲的线程可用则立即使用空闲线程执行任务如果没有空闲的线程可用就会创建一个新的线程执行任务当空闲线程闲置一段时间默认是60秒之后还未被使用那么就会进行销毁操作。 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;public class CachedThreadPoolExample {public static void main(String[] args) {// 创建可缓存的线程池ExecutorService executorService Executors.newCachedThreadPool();// 提交任务给线程池for (int i 0; i 5; i) {final int studentId i;executorService.execute(new Runnable() {Overridepublic void run() {System.out.println(学生 studentId 正在报名兴趣小组...);try {Thread.sleep(1000); // 模拟报名过程的时间消耗} catch (InterruptedException e) {e.printStackTrace();}System.out.println(学生 studentId 报名成功);}});}// 关闭线程池executorService.shutdown();} }输出结果 第二个是newFixedThreadPool 创建一个定长线程池。 该线程池中的线程数量始终不变。当有⼀个新的任务提交时线程池中若有空闲线程则⽴即执⾏。若没有则新的任务会被暂存在⼀个任务队列中待有线程空闲时便处理在任务队列中的任务。 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class FixedThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池大小为2ExecutorService executorService Executors.newFixedThreadPool(2);// 提交3个任务给线程池for (int i 0; i 3; i) {Runnable task new Task(i);executorService.submit(task);}// 所有任务执行完毕关闭线程池executorService.shutdown();}static class Task implements Runnable {private int taskId;public Task(int taskId) {this.taskId taskId;}Overridepublic void run() {System.out.println(Task taskId 执行中...);try {// 暂停Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Task taskId 执行完毕);}} }输出结果 第三个是newScheduledThreadPool 创建一个定长线程池支持定时及周期性任务执行。 import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit;public class ScheduledThreadPoolExample {public static void main(String[] args) {// 创建具有固定核心线程数的线程池ScheduledExecutorService executorService Executors.newScheduledThreadPool(2);// 延迟任务延迟3秒后仅执行一次executorService.schedule(new Runnable() {Overridepublic void run() {System.out.println(延迟任务开始执行....);}}, 3, TimeUnit.SECONDS);// 周期性任务每隔1秒执行一次executorService.scheduleAtFixedRate(new Runnable() {Overridepublic void run() {System.out.println(周期性任务开始执行....);}}, 0, 1, TimeUnit.SECONDS);// 等待一段时间后关闭线程池try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}executorService.shutdown();System.out.println(主线程执行完毕。。。);} }输出结果 第四个是newSingleThreadExecutor 创建一个单线程的线程池它只会使用一个线程执行任务可以保证任务的执行顺序。 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class SingleThreadExecutorExample {public static void main(String[] args) {System.out.println(开始执行);// 创建单线程的线程池ExecutorService executorService Executors.newSingleThreadExecutor();// 提交任务给线程池for (int i 0; i 5; i) {final int taskNumber i;executorService.execute(new Runnable() {Overridepublic void run() {System.out.println(Task taskNumber 执行中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Task taskNumber 执行完毕);}});}// 关闭线程池executorService.shutdown();System.out.println(主线程执行完毕等待线程池任务执行。);} }输出结果 面试官ThreadPoolExecutor的核心参数有哪些 候选人ThreadPoolExecutor 类中提供的四个构造⽅法。我们来看最⻓的那个其余三个都是在这个构造⽅法的基础上产⽣其他⼏个构造⽅法说⽩点都是给定某些默认参数的构造⽅法⽐如默认制定拒绝策略是什么 /*** ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。*/new ThreadPoolExecutor(corePoolSize , maximumPoolSize , keepAliveTime , milliseconds , runnableTaskQueue , handler);在线程池中一共有7个核心参数 corePoolSize 线程池的基本大小 - 核⼼线程数定义了最⼩可以同时运⾏的线程数量maximumPoolSize 线程池最大数量 - 线程池允许创建的最大线程数。最大线程数目核心线程救急线程的最大数目keepAliveTime 线程活动保持时间 - 线程池的工作线程空闲后保持存活的时间timeUnit 线程活动保持时间的单位 - 如秒、毫秒等runnableTaskQueue 任务队列 - 用于保存等待执行的任务的阻塞队列 《Java 并发编程的艺术》 9.2.1节 详情见本模块最后一个issue… 可以选择以下几个阻塞队列 ArrayBlockingQueue:一个基于数组结构的有界阻塞队列此队列按 FIFO(先进先出)原则对元素进行排序。LinkedBlockingQueue:一个基于链表结构的阻塞队列此队列按 FIFO 排序元素吞吐量通常要高于 ArrayBlockingQueue。 threadFactory 线程工厂 - 可以定制线程对象的创建例如设置线程名字、是否是守护线程等 handler 拒绝策略 - 当队列和线程池都满时会触发拒绝策略默认策略是AbortPolicy表明无法处理新任务时抛出异常 《Java 并发编程的艺术》9.2.1节 在jdk1.5中Java线程池框架提供了以下4种策略。 AbortPolicy:直接抛出异常。CallerRunsPolicy:只用调用者所在线程来运行任务。DiscardOldestPolicy:丢弃队列里最近的一个任务并执行当前任务。DiscardPolicy:不处理丢弃掉。 面试官线程池的执行原理知道吗 候选人当提交一个新任务到线程池时线程池的处理流程如下。 1)线程池判断核心线程池里的线程是否都在执行任务。如果不是则创建一个新的工作线程核心线程来执行任务。如果核心线程池里的线程都在执行任务则进入下个流程。 2)线程池判断工作队列是否已经满。如果工作队列没有满则将新提交的任务存储在这个任务队列里。如果任务队列满了则进入下个流程。 3)线程池判断线程池的线程是否都处于工作状态。如果没有则创建一个新的救急线程来执行任务。如果已经满了则交给饱和策略来处理这个任务。 4)如果核心线程或救急线程完成任务会检查任务队列中是否有需要执行的任务如果有就核心线程或救急线程会执行任务。 面试官为什么不建议使用Executors创建线程池呢 候选人 好的其实这个事情在阿里提供的最新开发手册《Java开发手册-嵩山版》中也提到了。 主要原因是如果使用Executors创建线程池的话它允许的请求队列默认长度是Integer.MAX_VALUE这样的话有可能导致堆积大量的请求从而导致OOM内存溢出。 所以我们一般推荐使用ThreadPoolExecutor来创建线程池这样可以明确规定线程池的参数避免资源的耗尽。 面试官 执⾏execute()⽅法和submit()⽅法的区别是什么呢 候选人 execute()⽅法⽤于提交不需要返回值的任务所以⽆法判断任务是否被线程池执⾏成功与否 threadsPool.execute(new Runnable() {Overridepublic void run() {// do something}});submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象通过这个 Future 对象可以判断任务是否执⾏成功并且可以通过 Future 的 get() ⽅法来获取返回值 get() ⽅法会阻塞当前线程直到任务完成⽽使⽤ getlong timeout TimeUnitunit⽅法则会阻塞当前线程⼀段时间后⽴即返回这时候有可能任务没有执⾏完。 FutureObject future executor.submit(harReturnValuetask);try{Object result future.get();} catch(InterruptedException e){// 处理中断异常} catch (ExecutionException e){// 处理无法执行任务异常} finally {executor.shutdown();}面试官Java 里的阻塞队列都有哪些能简单说说吗 候选人源自《Java并发编程的艺术》 6.3.2节 JDK7提供了7个阻塞队列如下。 ArrayBlockingQueue一个由数组结构组成的有界阻塞队列。LinkedBlockingOueue一个由链表结构组成的无界阻塞队列。PriorityBlockingQueue一个支持优先级排序的无界阻塞队列。DelayQueue一个使用优先级队列实现的无界阻塞队列。SynchronousOueue一个不存储元素的阻塞队列。LinkedTransferQueue一个由链表结构组成的无界阻塞队列。LinkedBlockingDeque一个由链表结构组成的双向阻塞队列。 下面重点介绍3类阻塞队列其他的队列还请大家自行查阅书籍网站没有先后次序之分。 ArrayBlockingQueue ArrayBlockingOueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。 LinkedBlockingQueue LinkedBlockingOueue 是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出(FIFO)的原则对元素进行排序。 LinkedBlockingDeque LinkedBlockingDeque 是一个由链表结构组成的双向阻寒队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口在多线程同时入队时也就减少了一半的竞争。 4 线程使用场景问题 面试官如果控制某一个方法允许并发访问线程的数量 候选人 在jdk中提供了一个Semaphore类信号量 它提供了两个方法semaphore.acquire() 请求信号量可以限制线程的个数是一个正数如果信号量是-1,就代表已经用完了信号量其他线程需要阻塞了 第二个方法是semaphore.release()代表是释放一个信号量此时信号量的个数1 面试官好的那该如何保证Java程序在多线程的情况下执行安全呢 候选人 嗯刚才讲过了导致线程安全的原因。要解决多线程环境下的执行安全问题JDK提供了多种工具和机制来帮助我们 原子性问题可以通过使用java.util.concurrent.atomic包下的原子类如AtomicInteger、AtomicLong等来解决。此外synchronized关键字和ReentrantLock也可以用来确保代码块的原子性执行。可见性问题synchronized关键字和volatile关键字都可以用来确保一个线程对共享变量的修改能够被其他线程看到。使用显式锁如ReentrantLock时配合Condition对象也可以保证可见性。有序性问题通过遵循Happens-Before规则可以保证多线程环境下操作的有序性。synchronized关键字和volatile关键字同样可以用来保证有序性。 面试官你在项目中哪里用了多线程 候选人 嗯~~我想一下当时的场景[根据自己简历上的模块设计多线程场景] 参考场景一 es数据批量导入 在我们项目上线之前我们需要把数据量的数据一次性的同步到es索引库中但是当时的数据好像是1000万左右一次性读取数据肯定不行oom异常如果分批执行的话耗时也太久了。所以当时我就想到可以使用线程池的方式导入利用CountDownLatchFuture来控制就能大大提升导入的时间。 参考场景二 在我做那个xx电商网站的时候里面有一个数据汇总的功能在用户下单之后需要查询订单信息也需要获得订单中的商品详细信息可能是多个还需要查看物流发货信息。因为它们三个对应的分别三个微服务如果一个一个的操作的话互相等待的时间比较长。所以我当时就想到可以使用线程池让多个线程同时处理最终再汇总结果就可以了当然里面需要用到Future来获取每个线程执行之后的结果才行 参考场景三 《黑马头条》项目中使用的 我当时做了一个文章搜索的功能用户输入关键字要搜索文章同时需要保存用户的搜索记录搜索历史这块我设计的时候为了不影响用户的正常搜索我们采用的异步的方式进行保存的为了提升性能我们加入了线程池也就说在调用异步方法的时候直接从线程池中获取线程使用 5 其他 面试官讲一下JMM(Java内存模型) 候选人 Java线程间的通信由JMM控制JMM定义了共享内存中多线程程序读写操作的行为规范通过这些规则来规范对内存的读写操作从而保证指令的准确性内存可见性。 《Java并发编程的艺术》 3.1.3节 JMM 属于语言级的内在模型它确保在不同的编译器和不同的处理器平台之上通过禁止特定类型的编译器重排序和处理器重排序为程序员提供一致的内存可见性保证。 JMM把内存分成两块一块是本地内存一块是主内存。线程跟线程之间相互隔离线程跟线程交互需要通过主内存。 面试官JMM 可能会导致数据不一致怎么理解 候选人 在 JDK1.2 之前Java 的内存模型实现总是从主存即共享内存读取变量是不需要进⾏特别的注意的。⽽在当前的 Java 内存模型下线程可以把变量保存本地内存⽐如机器的寄存器中⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉造成数据的不⼀致。 要解决这个问题可以把变量声明为 volatile 这就指示 JVM 这个变量是共享且不稳定的每次使⽤它都到主存中进⾏读取。保证变量的可⻅性。 面试官聊聊happens-before与JMM的关系 候选人 happens-before是JMM最核心的概念。对应Java程序员来说理解happens-before是理解JMM的关键。 JSR-133 使用 happens-before 的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内也可以是在不同线程之间。因此JMM 可以通过happens-before 关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在 happens-before 关系尽管a操作和b操作在不同的线程中执行但 JMM 向程序员保证a操作将对 b操作可见)。 《JSR-133: Java Memory Model and Thread Specification》对happens-before 关系的定义如下。 如果一个操作 happens-before 另一个操作那么第一个操作的执行结果将对第二个操作可见而且第一个操作的执行顺序排在第二个操作之前。两个操作之间存在 happens-before 关系并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果与按 happens-before关系来执行的结果一致那么这种重排序并不非法(也就是说JMM 允许这种重排序)。 面试官你知道happens-before有哪些应用场景吗 候选人happens-before 的应用场景主要是在多线程编程中用于确保线程之间的操作顺序和可见性。以下是一些常见的应用场景 线程同步happens-before 可以用于保证线程之间的同步操作的正确性。例如在使用 synchronized 或 Lock 机制进行线程同步时happens-before规则可以确保一个线程的解锁操作 happens-before 后续线程的加锁操作从而保证线程之间的同步性。 volatile 变量happens-before 可以用于保证对 volatile 变量的写操作对后续线程的读操作可见。因为 volatile 变量具有可见性所以对一个 volatile 变量的写操作 happens-before 后续线程对该变量的读操作确保了变量的可见性。 线程间通信happens-before 可以用于确保线程间通信的正确性。例如使用 wait/notify 或 await/signal 机制进行线程间的等待和唤醒操作时happens-before 可以确保等待线程在接收通知之前必须看到发送通知的线程对共享数据的修改。 线程安全性happens-before 可以用于保证线程安全性。例如在使用 synchronized 或 Lock 机制保护共享资源时happens-before 可以确保一个线程的写操作 happens-before 后续线程的读操作从而保证线程安全。 线程的启动和终止happens-before 可以用于确保线程的启动操作 happens-before 后续线程的操作以及线程的终止操作 happens-before 其他线程对该线程的操作。 面试官谈谈你对ThreadLocal的理解 候选人 通常情况下我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢 JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦盒⼦中可以存储每个线程的私有数据。从而避免了线程间竞争的安全问题。 《Java并发编程的艺术》 4.3.6节 ThreadLocal即线程变量是一个以 ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上也就是说一个线程可以根据一个 ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过 set(T)方法来设置一个值在当前线程下再通过 get()方法获取到原先设置的值。 面试官好的那你知道ThreadLocal的底层原理实现吗 候选人 在ThreadLocal内部维护了一个 ThreadLocalMap 类型的成员变量用来存储资源对象。 当调用 set 方法就是以 ThreadLocal 自己作为 key资源对象作为 value放入当前线程的 ThreadLocalMap 集合中。 当调用 get 方法就是以 ThreadLocal 自己作为 key到当前线程中查找关联的资源值。 当调用 remove 方法就是以 ThreadLocal 自己作为 key移除当前线程关联的资源值。 面试官好的那关于ThreadLocal会导致内存溢出这个事情了解吗 候选人 因为ThreadLocalMap 中的 key 被设计为弱引用而value是一个强引用。所以在垃圾回收的时候key 会被清理掉⽽ value 不会被清理掉。这样⼀来 ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话value 永远⽆法被 GC 回收这个时候就可能会产⽣内存泄露OOM。 ThreadLocalMap 实现中已经考虑了这种情况在调⽤ set() 、 get() 、 remove() ⽅法的时候会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() 释放key。 拓展强引用与弱引用《深入理解Java虚拟机JVM高级特性与最佳实践》 强引用是最传统的“引用”的定义是指在程序代码之中普遍存在的引用赋值即类似“ Object obj new Object()”这种引用关系。无论任何情况下只要强引用关系还存在垃圾收集器就永远不会回收掉被引用的对象。弱引用也是用来描述那些非必须对象但是它的强度比软引用更弱一些被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作无论当前内存是否足够都会回收掉只被弱引用关联的对象。
http://www.dnsts.com.cn/news/54120.html

相关文章:

  • 确定网站设计公司简报上海松江 网站建设公司
  • 保定市做网站的公司做网站设计的电脑需要什么配置
  • 购物网站建设的思路阿里云应用镜像wordpress
  • 网站前台如何刷新做物流哪个网站推广好
  • 温州市建设监理协会网站优化网站制作
  • 做陶瓷公司网站做网站 pc端与手机端兼容
  • 中学生做网站郑州设计工作室
  • 开发网站公司都需要什么岗位人员wordpress文章分类目录
  • 建设厅网站举报flash网站的优缺点
  • 让网站建设便宜到底设计logo图片
  • 做ppt高手 一定要常去这八个网站wordpress专栏插件
  • 婚介网站开发中国纪检监察
  • 网站1g空间多少钱做网站怎么移动图片
  • 深圳最好的营销网站建设公司哪家好大连龙采做网站行不行
  • 建设网站采用的网络技术软件应用下载安装
  • 如何网上建设网站WordPress状态栏替换
  • 茂名网站制作策划国有企业参股管理暂行办法
  • 做网站还有流量么网站建设网站建设哪里有
  • 建网站和建网页的区别镇海seo关键词优化费用
  • 儿童 网站模板网站打开速度慢优化
  • 杭州平台网站建设青岛百度排名优化
  • 北京智能模板建站网页设计代码源
  • 一级a做爰片付费网站站长工具app官方下载
  • 网站维护中 源码短视频培训要多少学费
  • 成都网站seo费用大连seo外包平台
  • 企业形象网站解决方案荥阳网站建设荥阳
  • 网站建设 站内搜索wordpress萌
  • 自己服务器建网站个人网站建设方案书实例
  • 陕西省住房和城乡建设厅执业资格注册中心网站公司网站制作的教程
  • 英语作文网站大连做网站首选领超科技