锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

java高级进阶面试题

时间:2023-09-07 18:07:00 x9315wp集成电路

JVM 面试题目
请简单描述一下 JVM 运行时数据区包括哪些部分?
JVM 在执行 Java 在程序过程中,将其管理的内存分为几个不同的区域,这些组成部分
有的是线程私有,有的是线程共享
私有线程:程序计数器、虚拟机栈、本地方法栈
线程共享:方法区,堆叠
2.JVM 如何判断对象可回收?
可达性分析算法
该算法的基本思想是通过一系列称为算法 “GC Roots” 以对象为起点,从这些部分开始
点开始向下搜索,节点通过的路径称为引用链,当对象到达时 GC Roots 没有引用链
如果连接,则证明该对象不可用,可回收。
3.方法区主要存储哪些信息?
方法区是所有线程共享。主要用于存储信息、常量池、方法数据、方法代码等。
法区逻辑上属于堆的一部分,但为了区分堆,通常又称非堆。
4.请简单描述一下 JVM 几种异常?
主要有两种:StackOverFlowError 和 OutOfMemoryError.
StackOverFlowError:
当前线程请求栈的深度超过当前线程请求栈 Java 当虚拟机栈的最大深度被抛出时,
StackOverFlowError 异常。
OutOfMemoryError:

  1. 当线程请求栈内存用完时,无法动态扩展,此时抛出 OutOfMemoryError 异
    常。
  2. 堆积内存或永久代代/元空间不足,无法同时分配对象或存储数据
    永久代/元空间无法扩大,此时抛出 OutOfMemoryError 异常。
  3. 垃圾回收器占用 JVM 中 98%的资源回收效率低于 2%,JVM 会抛出
    OutOfMemoryError,
    5.在默认参数下,如果 Eden 区的大小为 80M,求堆空
    间总大小?
    可按比例计算(Eden:survivor1:survivor2 =8:1:1),两个 survivor 区各 10M,
    新生代 100M。默认情况下,老年人是年轻人的两倍,即 200M。那堆总大小就是 3000M。
    6.什么是程序计数器?为什么? JVM 需要它?
    程序计数器记录当前线程正在执行的字节码的地址或行号。
    JVM 线程切换的主要功能是确保多线程 JVM 正常执行程序。
    7.请描述下 JVM 创建中对象需要多少步骤?
    1)检查加载
    首先执行相应的类加载过程。如果没有,则类加载。
    2)分配内存
    有两种分配方法:指针碰撞和空闲列表,Java 如果堆叠空间规则,则采用指针碰撞,采摘不规则
    用空闲列表
    3)内存空间的初始化
    JVM需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。
    4)设置
    需要设置虚拟机对象,比如这个对象是什么样的例子,如何找到元
    数据信息、对象哈希码、对象 GC 分代年龄等信息。这些信息存储在对象的对象头中。
    5)对象的初始化
    最后,调用结构方法。
    8.JVM 访问对象访问定位?HostSpot 版本中用
    哪种?
    句柄和直接指针,
    句柄: 若使用句柄,则 Java 堆中将一块内存划分为句柄池,
    reference 存储在对象的句柄地址中,句柄包含对象的实例数据和类型数据
    具体地址信息;
    直接指针: 如果使用直接指针访问,那么 Java 必须考虑如何放置堆对象的布局
    访问类型数据的相关信息 reference 直接存储在对象的地址中。
    HotSpot 使用直接指针,使用直接指针访问,节省了一个
    次指针定位时间费。
    9.JVM 有哪些引用?
  4. 强引用
    我们过去使用的大多数引用实际上都是强引用,这是最常用的引用。如果一个对象
    强引用类似于必不可少的日用品,垃圾回收器永远不会回收。当内存空时 间不
    足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误导致程序异常终止,不会随意回收工具
    有很强的引用对象来解决内存不足的问题。
  5. 软引用(SoftReference)
    如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃
    如果内存空间不足,垃圾回收器就不会回收这些对象的内存。只要垃圾回收器
    该对象可以在不回收的情况下使用。软引用可用于实现内存敏感的高速缓存。
    软引用和引用队列(ReferenceQueue)如果软引用引用的对象联合使用
    垃圾回收,JAVA 虚拟机将软引用添加到相关的引用队列中。
  6. 弱引用(WeakReference)
    如果一个对象只有弱引用,它类似于可有可无的日用品。弱引用和软引用的区别
    只有弱引用对象才有更短的生命周期。扫描垃圾回收器线程 所管辖的内
    在存储区域的过程中,一旦发现只有弱引用对象,无论当前的内存空间是否足够,都将被回收利用
    它的内存。但是,由于垃圾回收器是优先级很低的线程, 因此不一定会很快发现那些
    只有弱引用对象。
    弱引用和引用队列(ReferenceQueue)联合使用,如果引用的对象被弱引用
    垃圾回收,Java 虚拟机将弱引用添加到与之相关的引用队列中。
  7. 虚引用(PhantomReference)
    "虚引用"顾名思义,就是形同虚设,不同于其他几种引用,虚引不会决定对象
    生命周期。如果一个对象只持有虚假引用,那么它可能在任何时候都与没有任何引用一样
    垃圾回收。
    主要用于跟踪垃圾回收对象的活动。
    在程序设计中,除了强引用外,还使用软引用,因为软引用可以加速 JVM 对垃
    垃圾内存的回收速度可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产

    10.JVM 垃圾收集的算法和特点是什么?
  8. 标记-清除算法
    标记清除算法分为标记和清除阶段:首先标记所有需要回收的对象,并标记在标记中
    记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两
    一个明显的问题:
    1)效率问题
    2)空间问题(清除标记后会产生大量不连续的碎片)
  9. 复制算法
    为了解决效率问题,出现了复制收集算法。它可以将内存分为两个相同大小的块,每个块
    第二次使用其中一个。使用这块内存后,将存活对象复制到另一块,然后
    一次清理使用的空间。这样,每次内存回收都是内存范围的一半。
  10. 标记-整理算法
    根据老年人的特点,标记过程仍然与标记-清除算法相同,但
    后续步骤不是直接回收可回收对象,而是将所有存活对象移动到一端,然后直接清理干净
    端边界以外的内存。
    11.HotSpot 为什么中堆要分为新生代和老年代?
    将 Java 堆分为新生代和老年人,因此我们可以根据的特点选择合适的垃圾
    收集算法。
    例如,在新一代,每次收集都会有大量的对象死亡,所以你可以选择复制算法,只需付费
    每次垃圾收集都可以收取少量对象的复制成本。但是老年对象存活率比较高,而且
    没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法
    收集垃圾
    12.JVM 哪些情况需要初始化?
    虚拟机规范严格规定,只有五种情况必须立即初始化:
    1。使用 new 当关键字实例化对象、读取或设置类静态字段时,已经调整了
    当使用一种静态方法时。
    2.使用 Java.lang.reflect 当包装方法反射类时,如果类没有初始化,则
    首先要触发其初始化。
    3.初始化一个类时,如果发现其父类没有初始化,就会先初始化其父类。
    4.当虚拟机启动时,用户需要指定要执行的主要类别(即包含 main()方法
    类),虚拟机会首先初始化;
    5.使用动态语言支持时,如果一个 Java.lang.invoke.MethodHandle 例子的最后解
    析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 方法句柄,这个方法句柄
    如果相应的类别没有初始化,则需要先触发其初始化。
    13.请简单描述一下双亲委派模型?
    双亲委派模型(Pattern Delegation Model),除顶层启动式加载器外,其他要求
    所有类加载器都应该有自己的父加载器。父子关系通常是子类通过组合而不是继承的
    关系重用父加载器代码。
    双亲委派模型的工作流程: 如果一个类加载器收到类加载请求,请先委托此请求
    派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),
    只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己加载。
  11. CMS 收集器的特点是什么?
    一般优点:并发收集,低停顿。CMS 收集器对 CPU 资源敏感,CMS 收集器不
    浮动垃圾可以处理。
    CMS 收集器是基于标记-清除算法实现的,其运行过程与前几种收集器相比
    整个过程分为更复杂的过程 4 步骤包括:
    1.初始标记-短暂,只是标记 GC Roots 可以直接关联的对象非常快。
    2.并发标记-与用户应用程序同时进行 GC Roots 跟踪过程,从标记 GCRoots
    所有开始关联的对象开始遍历整个可达分析路径的对象。这需要很长时间,所以使用并发处
    理论(垃圾回收器线程和用户线程同时工作)
    3.重新标记-短暂。为了纠正并发标记期间用户程序继续运行导致的标记变化
    这一阶段对象标记的停顿时间一般比初始标记阶段稍长,但远远超过并购
    发标记时间短。
    4、并发清除,由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以
    与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起
    并发执行的。
    CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用
    户的影响较大。
    浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新
    的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,
    只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集
    器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%),如果
    预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启
    用 Serial Old 来替代 CMS。
    15.G1 收集器有哪些特点?
    在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。
    使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 堆划分
    为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和
    老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。每一个区域
    可以通过参数-XX:G1HeapRegionSize=size 来设置。
    Region 中还有一块特殊区域 Humongous 区域,专门用于存储大对象,一般只要认为一
    个对象超过了 Region 容量的一般可认为是大对象,如果对象超级大,那么使用连续的 N 个
    Humongous 区域来存储。
    并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者
    CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执
    行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
    分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其
    他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已
    经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
    空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”
    算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无
    论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可
    用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而
    提前触发下一次 GC。
    同时 G1 追求可预测的停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年
    龄来达到这个目标时间。
    一般在 G1 和 CMS 中间选择的话平衡点在 6~8G,只有内存比较大 G1 才能发挥优势。
    16、JVM 类加载机制有哪几步,分别每一步做了什么
    工作?
    包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、
    初始化(Initialization)阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)。
    加载
    什么是需要开始类第一个阶段“加载”,虚拟机规范没有强制约束,这点交给虚拟机的
    具体实现来自由把控。
    “加载 loading”阶段是整个类加载(class loading)过程的一个阶段。
    虚拟机需要完成以下 3 件事情:
    1)通过一个类的全限定名来获取定义此类的二进制字节流。
    2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3)在内存中生成一个代表这个类的 Java.lang.Class 对象,作为方法区这个类的各种
    数据的访问入口。
    验证
    是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符
    合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会
    完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
    准备
    是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方
    法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行
    内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在
    对象实例化时随着对象一起分配在 Java 堆中。其次,这里所说的初始值“通常情况”下是
    数据类型的零值,
    解析
    是虚拟机将常量池内的符号引用替换为直接引用的过程。部分详细内容见解析
    初始化
    初始化阶段,虚拟机规范则是严格规定了有且只有 6 种情况必须立即对类进行“初始
    化”(而加载、验证、准备自然需要在此之前开始):
    1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没
    有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:
    使用 new 关键字实例化对象的时候。
    读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字
    段除外)的时候
    调用一个类的静态方法的时候。
    2)使用 Java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初
    始化,则需要先触发其初始化。
    3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父
    类的初始化。
    4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),
    虚拟机会先初始化这个主类。
    5)当使用 JDK 1.7 的动态语言支持时,如果一个 Java.lang.invoke.MethodHandle 实
    例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且
    这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
    6)当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)
    时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
    初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序
    可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,
    则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角
    度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是
    由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并
    产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
    <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,
    也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
    并发编程面试题目
    1.在 Java 中守护线程和用户线程的区别?
    Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
    任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool on);
    true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在
    Thread.start()之前调用,否则运行时会抛出异常。
    两者的区别:
    唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部
    的 User Thread 已经结束,Daemon 没有可服务的线程,JVM 关闭。
    扩展:Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程
    2.线程与进程的区别
    进程是操作系统配资源的最小单元,线程是操作系统调度的最小单元。
    一个程序至少有一个进程,一个进程至少有一个线程。
    3.什么是多线程中的上下文切换
    多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了
    让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU 发生的切换数
    据等就是上下文切换。
    4.死与活的区别,死锁与饥饿的区别?
    死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一
    种互相等待的现象,若无外力作用,它们都将无法推进下去。
    产生死锁的必要条件:
    互斥条件:所谓互斥就是进程在某一时间内独占资源。
    请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失
    败,尝试,失败。
    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处
    于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
    饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的
    状态。
  12. synchronized 底层实现原理
    synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同
    步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令 monitorenter 和
    monitorexit 来实现,相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。
    JVM 就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法
    的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获
    取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线
    程都无法再获得同一个 monitor 对象。
    注意,这个问题可能会接着追问,Java 对象头信息,偏向锁,轻量锁,重量级锁及其
    他们相互间转化。
    6.什么是线程组,为什么在 Java 中不推荐使用?
    ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可
    以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
    1.线程组 ThreadGroup 对象中比较有用的方法是 stop、resume、suspend 等方法,由
    于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程
    组本身的应用价值就大打折扣了。
    2.线程组 ThreadGroup 不是线程安全的,这在使用过程中获取的信息并不全是及时有
    效的,这就降低了它的统计使用价值。
    7.什么是 Executors 框架?为什么使用 Executor 框
    架?
    Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
    每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗
    资源的。
    调用 new Thread()创建的线程缺乏管理,而且可以无限制的创建,线程之间的相互
    竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统
    资源。
    接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期
    执行、线程中断等都不便实现。
    8.在 Java 中 Executor 和 Executors 的区别?
    Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需
    求。
    Executor 接口对象能执行我们的线程任务。
    ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能
    获得任务执行的状态并且可以获取任务的返回值。
    使用 ThreadPoolExecutor 可以创建自定义线程池。
    9.什么是原子操作?在 Java Concurrency API 中有
    哪些原子类(atomic classes)?
    原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
    处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
    在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作——Compare & Set,
    或 是 Compare & Swap ,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。
    Java.util.concurrent.atomic 下提供了大量的原子操作类,比如原子类:AtomicBoolean,
    AtomicInteger, AtomicLong ,AtomicReference ,原子数组: AtomicIntegerArray,
    AtomicLongArray,AtomicReferenceArray ,原子属性更新器:AtomicLongFieldUpdater,
    AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  13. Java Concurrency API 中的 Lock 接口(Lock
    interface)是什么?对比 synchronized 它有什么
    优势?
    Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。
    他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件
    对象。
    它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以让线程尝
    试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在不同的范围,以不同
    的顺序获取和释放锁。
    整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock
    方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列
    的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平
    锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
  14. 什么是阻塞队列?阻塞队列的实现原理是什
    么?如何使用阻塞队列来实现生产者-消费者模
    型?
    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
    这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列
    满时,存储元素的线程会等待队列可用。
    阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者
    是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿
    元素。
    JDK7 提供了 7 个阻塞队列。在实现上,主要是利用了 Condition 和 Lock 的等待通知模
    式。
  15. 什么是 Callable 和 Future?
    Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结
    果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返
    回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
    可以认为是带有回调的 Runnable。
    Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
    用于产生结果,Future 用于获取结果。
  16. 什么是 FutureTask?
    在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、
    查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算
    尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的
    对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来
    执行。
  17. 什么是竞争条件?
    当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序
    时,则我们认为这发生了竞争条件(race condition)。
  18. 启动线程的方式有几种?
    两种,启动线程的方式有:
    1、X extends Thread;,然后 X.start
    2、X implements Runnable;然后交给 Thread 运行
  19. 为什么我们调用 start()方法时会执行 run()
    方法,为什么我们不能直接调用 run()方法?
    当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。
    但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,
    只会把 run 方法当作普通方法去执行。
  20. 执行两次 start 方法可以吗?
    不行,程序会抛出 IlegelState 异常。
  21. 在 Java 中 CycliBarriar 和 CountdownLatch 有
    什么区别?
    CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。
  22. 什么是不可变对象,它对写并发应用有什么帮
    助?
    不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对
    象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
    不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变
    类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
    不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们
    的状态无法修改,这些常量永远不会变。
    不可变对象永远是线程安全的。
    只有满足如下状态,一个对象才是不可变的;
    它的状态不能在创建后再被修改;
    所有域都是 final 类型;并且,
    它被正确创建
  23. notify()和 notifyAll()有什么区别?
    当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall,使用 notifyall,可
    以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
    如果没把握,建议 notifyAll,防止 notigy 因为信号丢失而造成程序异常。
  24. 什么是可重入锁(ReentrantLock)?谈谈它的
    实现。
    线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、
    ReentrantLock 都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程
    是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,
    表示线程已经彻底释放锁。
  25. 当一个线程进入某个对象的一个 synchronized
    的实例方法后,其它线程是否可进入此对象的其它
    方法?
    如果其他方法没有 synchronized 的话,其他线程是可以进入的。
    所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
  26. 乐观锁和悲观锁的理解及如何实现,有哪些实现
    方式?
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在
    拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java 里面的同步
    原语 synchronized 关键字的实现是悲观锁。
    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不
    会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本
    号等机制。在 Java 中 j 原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
    乐观锁的实现方式:
    使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不
    一致时可以采取丢弃和再次尝试的策略。
    Java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量
    时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而
    是被告知这次竞争中失败,并可以再次尝试。
  27. 什么是 CAS 操作,缺点是什么?
    CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不
    做任何事儿,但是要返回原值是多少。每一个 CAS 操作过程都包含三个运算符:一个内存地
    址 V,一个期望的值 A 和一个新值 B,操作的时候如果这个地址上存放的值等于这个期望的
    值 A,则将地址上的值赋为新值 B,否则不做任何操作。
    CAS 缺点:
    ABA 问题:
    比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,
    并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进
    行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可
    能存在潜藏的问题。从 Java1.5 开 始 JDK 的 atomic 包里提供了一个类
    AtomicStampedReference 来解决 ABA 问题。
    循环时间长开销大:
    对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多
    的 CPU 资源,效率低于 synchronized。
    只能保证一个共享变量的原子操作:
    当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是
    对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
  28. SynchronizedMap 和 ConcurrentHashMap 有什么
    区别?
    SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为
    map。
    ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
  29. 写时复制容器可以用于什么应用场景?
    CopyOnWrite 并发容器用于对于绝大部分访问都是读,且只是偶尔写的并发场景。比如
    白名单,黑名单,商品类目的访问和更新场景。
    透露的思想
    读写分离,读和写分开
    最终一致性
    使用另外开辟空间的思路,来解决并发冲突
  30. volatile 有什么用?能否用一句话说明下
    volatile 的应用场景?
    volatile 保证内存可见性和禁止指令重排。
    volatile 用于多线程环境下的一写多读,或者无关联的多写。
  31. 为什么代码会重排序?
    在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能
    随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
    在单线程环境下不能改变程序运行的结果;
    存在数据依赖关系的不允许重排序
  32. 在 Java 中 wait 和 sleep 方法的不同?
    最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间
    交互,sleep 通常被用于暂停执行。
  33. 一个线程运行时发生异常会怎样?
    如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于
    处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断
    的时候 JVM 会使用 Thread.getUncaughtExceptionHandler ()来查询线程的
    UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException
    ()方法进行处理。
  34. 为什么 wait, notify 和 notifyAll 这些方法不
    在 thread 类里面?
    JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线
    程需要等待某些锁那么调用对象中的 wait()方法就有意义了。如果 wait()方法定义在
    Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和
    notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
  35. 什么是 ThreadLocal 变量?
    ThreadLocal 是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线
    程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
    其实现原理是,在 Thread 中有一个 ThreadLocalMap 类型的成员变量,线程的变量副
    本就放在这个 ThreadLocalMap 中,这个 ThreadLocalMap 就是以 ThreadLocal 为 key,线程
    的变量副本为 value。
  36. Java 中 interrupted 和 isInterrupted 方法的
    区别?
    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者
    不会。Java 多线程的中断机制是用内部标识来实现的,调用 Thread.interrupt()来中断
    一个线程就会设置中断标识为 true。当中断线程调用静态方法 Thread.interrupted()来
    检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted()用来查询其它线程
    的中断状态且不会改变中断状态标识。
  37. 为什么wait和notify方法要在同步块中调用?
    主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出
    IllegalMonitorStateException 异常。
  38. 为什么你应该在循环中检查等待条件?
    处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,
    程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原
    来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它
    可能会改变。这就是在循环中使用 wait()方法效果更好的原因
  39. 怎么检测一个线程是否拥有锁?
    在 Java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前
    线程拥有某个具体对象的锁。
  40. 你如何在 Java 中获取线程堆栈信息?
    kill -3 [Java pid]
    不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3 tomcat
    pid, 输出堆栈到 log 目录下。
    Jstack [Java pid]
    这个比较简单,在当前终端显示,也可以重定向到指定文件中。
    或者使用 Java 提供的拟机线程系统的管理接口 ManagementFactory.getThreadMXBean
    ()。
  41. Java 线程池中 submit() 和 execute()方法
    有什么区别?
    两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在
    Executor 接口中。
    而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService
    接口中,它扩展了 Executor 接口
  42. 你对线程优先级的理解是什么?
    每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但
    这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义
    线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级
    是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
    Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,
    如非特别需要,一般无需设置线程优先级。
  43. 你如何确保 main()方法所在的线程是 Java 程
    序最后结束的线程?
    可以使用 Thread 类的 join()方法(或者 CountDownLatch 工具类)来确保所有程序
    创建的线程在 main()方法退出前结束。
  44. 为什么 Thread 类的 sleep()和 yield ()方
    法是静态的?
    Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他
    处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们
    可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这
    些方法。
  45. 现在有 T1、T2、T3 三个线程,你怎样保证 T2 在
    T1 执行完后执行,T3 在 T2 执行完后执行?
    可以用 join 方法实现。
  46. 用 Java 写代码来解决生产者——消费者问题。
    阻塞队列实现即可,也可以用 wait 和 notify 来解决这个问题,或者用 Semaphore
  47. Java 中如何停止一个线程?
    使用共享变量的方式
    在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程
    用来作为是否中断的信号,通知中断线程的执行。
    使用 interrupt 方法终止线程
    如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?比如当一
    个线程由于需要等候键盘输入而被阻塞,或者调用 Thread.join()方法,或者 Thread.sleep
    ()方法,在网络中调用 ServerSocket.accept()方法,或者调用了 DatagramSocket.receive
    ()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该
    线程的共享变量设置为 true,但该线程此时根本无法检查循环标志,当然也就无法立即中
    断。所以应该尽量使用 Thread 提供的 interrupt()方法,因为该方法虽然不会中断一个
    正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束
    阻塞状态。
  48. JVM中哪个参数是用来控制线程的栈堆栈大小的
    -Xss
  49. 如果同步块内的线程抛出异常锁会释放吗?
  50. 单例模式的双重检查实现是什么?为什么并不
    安全?如何在 Java 中创建线程安全的
    Singleton?
    不安全的根本原因是重排序会导致未初始化完成的对象可以被其他线程看见而导致错
    误。创建安全的单例模式有:延迟占位模式、在声明的时候就 new 这个类的实例、枚举
  51. 请概述线程池的创建参数,怎么样合理配置一个
    线程池的参数?
    线程的构造方法如下:
    public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize,long
    keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory
    threadFactory,RejectedExecutionHandler handler)
    corePoolSize
    线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到
    当前线程数等于 corePoolSize;
    如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
    如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所
    有核心线程。
    maximumPoolSize
    线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的
    线程执行任务,前提是当前线程数小于 maximumPoolSize
    keepAliveTime
    线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,
    该参数只在线程数大于 corePoolSize 时才有用
    TimeUnit
    keepAliveTime 的时间单位
    workQueue
    用于保存等待执行的任务的阻塞队列,一般来说,我们应该尽量使用有界队列,因为
    使用无界队列作为工作队列会对线程池带来如下影响。
    1)当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线
    程池中的线程数不会超过 corePoolSize。
    2)由于 1,使用无界队列时 maximumPoolSize 将是一个无效参数。
    3)由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
    4)更重要的,使用无界 queue 可能会耗尽系统资源,有界队列则有助于防止资源耗尽,
    同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
    所以我们一般会使用, ArrayBlockingQueue 、 LinkedBlockingQueue 、
    SynchronousQueue、PriorityBlockingQueue。
    threadFactory
    创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度
    的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
    Executors 静态工厂里默认的 threadFactory,线程的命名规则是“pool-数字-thread-
    数字”。
    RejectedExecutionHandler
    线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,
    必须采取一种策略处理该任务,线程池提供了 4 种策略:
    (1)AbortPolicy:直接抛出异常,默认策略;
    (2)CallerRunsPolicy:用调用者所在的线程来执行任务;
    (3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    (4)DiscardPolicy:直接丢弃任务;
    当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如
    记录日志或持久化存储不能处理的任务。
    线程池的工作机制总结
    1)如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这
    一步骤需要获取全局锁)。
    2)如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue。
    3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务。
    4)如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调
    用 RejectedExecutionHandler.rejectedExecution()方法。
    合理地配置线程池
    要想合理地配置线程池,就必须首先分析任务特性
    要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
    ?任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
    ?任务的优先级:高、中和低。
    ?任务的执行时间:长、中和短。
    ?任务的依赖性:是否依赖其他系统资源,如数据库连接。
    性质不同的任务可以用不同规模的线程池分开处理。
    CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集
    型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2*Ncpu。
    混合型的任务,如果可以拆分,将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,
    只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐
    量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime
    ().availableProcessors()方法获得当前设备的 CPU 个数。
    对于 IO 型的任务的最佳线程数,有个公式可以计算
    Nthreads = NCPU * UCPU * (1 + W/C)
    其中:
    ?NCPU 是处理器的核的数目
    ?UCPU 是期望的 CPU 利用率(该值应该介于 0 和 1 之间)
    ?W/C 是等待时间与计算时间的比率
    等待时间与计算时间我们在 Linux 下使用相关的 vmstat 命令或者 top 命令查看。
    优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优
    先级高的任务先执行。
    执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,
    让执行时间短的任务先执行。
    依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,等待的时
    间越长,则 CPU 空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用 CPU。
    建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一
    点儿,比如几千。
    假设,我们现在有一个 Web 系统,里面使用了线程池来处理业务,在某些情况下,系
    统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数
    据库出现了问题,导致执行 SQL 变得非常缓慢,因为后台任务线程池里的任务全是需要向数
    据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。
    如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,
    导致整个系统不可用,而不只是后台任务出现问题。
  52. 请概述锁的公平和非公平,JDK 内部是如何实现
    的。
    公平锁是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平锁则是当
    前的锁状态没有被占用时,当前线程可以直接占用,而不需要等待。在实现上,非公平锁逻辑
    基本跟公平锁一致,唯一的区别是,当前线程不需要判断同步队列中是否有等待线程。
    非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之
    间存在着严重的延迟。而且,非公平锁能更充分的利用 cpu 的时间片,尽量的减少 cpu 空闲
    的状态时间。
    使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程占用(处
    理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁可以保证不会
    有线程被饿死。
  53. 请概述 AQS
    是用来构建锁或者其他同步组件的基础框架,比如 ReentrantLock 、
    ReentrantReadWriteLock 和 CountDownLatch 就是基于 AQS 实现的。它使用了一个 int 成员
    变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。它是 CLH 队列
    锁的一种变体实现。它可以实现 2 种同步方式:独占式,共享式。
    AQS 的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,
    同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几
    个可重写的方法,如 tryAcquire、tryReleaseShared 等等。
    这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件
    交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实
    现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
    这样就很好地隔离了使用者和实现者所需关注的领域。
    在内部,AQS 维护一个共享资源 state,通过内置的 FIFO 来完成获取资源线程的排队
    工作。该队列由一个一个的 Node 结点组成,每个 Node 结点维护一个 prev 引用和 next 引用,
    分别指向自己的前驱和后继结点,构成一个双端双向链表。
    同时与 Condition 相关的等待队列,节点类型也是 Node,构成一个单向链表。
  54. 请概述 volatile
    volatile 关键字的作用主要有两点:
    多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,
    保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据。但是
    volatile 不能保证操作的原子,对任意单个 volatile 变量的读/写具有原子性,但类似于
    ++这种复合操作不具有原子性。。
    代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能会出现一
    些意想不到的问题。使用 volatile 则会对禁止重排序,当然这也一定程度上降低了代码执
    行效率。
    同时在内存语义上,当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的
    共享变量值刷新到主内存,当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置
    为无效。线程接下来将从主内存中读取共享变量。
    在 Java 中对于 volatile 修饰的变量,编译器在生成字节码时,会在指令序列中插入
    内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。
    在具体实现上,volatile 关键字修饰的变量会存在一个“lock:”的前缀。它不是一种
    内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和高速缓存加锁,可以
    理解为 CPU 指令级的一种锁。
    同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的
    操作会使在其他 CPU 里缓存了该地址的数据无效。
  55. HashMap 和 HashTable 有什么区别?
    A:①、HashMap 是线程不安全的,HashTable 是线程安全的;
    ②、由于线程安全,所以 HashTable 的效率比不上 HashMap;
    ③、HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null,而
    HashTable 不允许;
    ④、HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时,扩大两倍,
    后者扩大两倍+1;
    ⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
  56. Java 中的另一个线程安全的与 HashMap 极其
    类似的类是什么?同样是线程安全,它与
    HashTable 在线程同步上有什么不同?
    A:ConcurrentHashMap 类(是 Java 并发包 Java.util.concurrent 中提供的一个线
    程安全且高效的 HashMap 实现)。
    HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
    而针对 ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中直接采用
    了 CAS(无锁算法)+ synchronized,也采用分段锁的方式并大大缩小了锁的粒度。
  57. HashMap & ConcurrentHashMap 的区别?
    A:除了加锁,原理上无太大区别。
    另外,HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。
    在数据结构上,红黑树相关的节点类
  58. 为什么 ConcurrentHashMap 比 HashTable 效
    率要高?
    A:HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,
    容易阻塞;
    ConcurrentHashMap
    JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个
    HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包
    含多个 HashEntry。
    JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实
    现 Map.Entry)。锁粒度降低了。
  59. 针对 ConcurrentHashMap 锁机制具体分析(JDK
    1.7 VS JDK 1.8)?
    JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结
    构,包括两个核心静态内部类 Segment 和 HashEntry。
    ①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对
    象守护每个散列映射表的若干个桶;
    ②、HashEntry 用来封装映射表的键-值对;
    ③、每个桶是由若干个 HashEntry 对象链接起来的链表。
    JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,
    直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过
    TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
  60. ConcurrentHashMap 在 JDK 1.8 中,为什么要
    使用内置锁 synchronized 来代替重入锁
    ReentrantLock?
    1、JVM 开发团队在 1.8 中对 synchronized 做了大量性能上的优化,而且基于 JVM 的
    synchronized 优化空间更大,更加自然。
    2、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开
    销更多的内存。
  61. ConcurrentHashMap 简单介绍?
    ①、重要的常量:
    private transient volatile int sizeCtl;
    当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;
    当为 0 时,表示 table 还没有初始化;
    当为其他正数时,表示初始化或者下一次进行扩容的大小。
    ②、数据结构:
    Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;
    TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于
    红黑树中存储数据;
    TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。
    ③、存储对象时(put() 方法):
    1.如果没有初始化,就调用 initTable() 方法来进行初始化;
    2.如果没有 hash 冲突就直接 CAS 无锁插入;
    3.如果需要扩容,就先进行扩容;
    4.如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接
    遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
    5.如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循

    6.如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。
    ④、扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。
    helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
    ⑤、获取对象时(get()方法):
    1.计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
    2.如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找
    该结点,匹配就返回;
    3.以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。
  62. ConcurrentHashMap 的并发度是什么?
    1.7 中程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。
    默认为 16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap 会使用
    大于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际并发度则
    为 32)。
    1.8 中并发度则无太大的实际意义了,主要用处就是当设置的初始容量小于并发度,将
    初始容量提升至并发度大小。
  63. 什么是锁消除和锁粗化?
    锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能
    存在共享数据竞争的锁进行消除。主要根据逃逸分析。
    锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一
    个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必
    要的性能损耗。锁粗化就是增大锁的作用域。
  64. 除了 ReetrantLock, JUC 下还有哪些并发工
    具?
    通常所说的并发包(JUC)也就是 Java.util.concurrent 及其子包,集中了 Java 并
    发的各种基础工具类,具体主要包括几个方面:
    提供了 CountDownLatch、CyclicBarrier、Semaphore 等,比 Synchronized 更加高
    级,可以实现更加丰富多线程操作的同步结构。
    提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通过类似快照机
    制实现线程安全的动态数组 CopyOnWriteArrayList 等,各种线程安全的容器。
    提供了 ArrayBlockingQueue 、 SynchorousQueue 或针对特定场景的
    PriorityBlockingQueue 等,各种阻塞队列实现。
    强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等。
  65. 什么是 Java 的内存模型,Java 中各个线程是
    怎么彼此看到对方的变量的?
    Java 的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存
    和从内存中取出这样的底层细节。
    此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和
    方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。
    Java 中各个线程是怎么彼此看到对方的变量的呢?Java 中定义了主内存与工作内存
    的概念:
    所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到
    的变量的主内存副本拷贝。
    线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内
    存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要
    通过主内存。
    Spring 面试题目
    1.你为什么使用 Spring?
    轻量级:Spring 在大小和透明性方面绝对属于轻量级的,基础版本的 Spring 框架大约
    只有 2MB。
    控制反转(IOC):Spring 使用控制反转技术实现了松耦合。依赖被注入到对象,而不是
    创建或寻找依赖对象。
    面向切面编程(AOP): Spring 支持面向切面编程,同时把应用的业务逻辑与系统的服
    务分离开来。
    容器:Spring 包含并管理应用程序对象的配置及生命周期。
    MVC 框架:Spring 的 web 框架是一个设计优良的 web MVC 框架,很好的取代了一些 web
    框架。
    事务管理:Spring 对下至本地业务上至全局业务(JAT)提供了统一的事务管理接口。
    异常处理:Spring 提供一个方便的 API 将特定技术的异常(由 JDBC, Hibernate, 或 JDO
    抛出)转化为一致的、Unchecked 异常。
  66. Spring 支持几种 bean 的作用域?
    1)singleton:默认,每个容器中只有一个 bean 的实例,单例的模式由 BeanFactory
    自身来维护。
    2)prototype:为每一个 bean 请求提供一个实例。
    3)request:为每一个网络请求创建一个实例,在请求完成以后,bean 会失效并被垃圾
    回收器回收。
    4)session:与 request 范围类似,确保每个 session 中有一个 bean 的实例,在 session
    过期后,bean 会随之失效。
    5)global-session:全局作用域,global-session 和 Portlet 应用相关。当你的应用
    部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声明让所有的 portlet 共
    用全局的存储变量的话,那么这全局变量需要存储在 global-session 中。全局作用域与
    Servlet 中的 session 作用域效果相同。
    3.请问 Spring 有几种自动装配模式?
    自动装配提供五种不同的模式供 Spring 容器用来自动装配 beans 之间的依赖注入:
    no:默认的方式是不进行自动装配,通过手工设置 ref 属性来进行装配 bean。
    byName:通过参数名自动装配,Spring 容器查找 beans 的属性,这些 beans 在 XML 配
    置文件中被设置为 byName。之后容器试图匹配、装配和该 bean 的属性具有相同名字的 bean。
    byType:通过参数的数据类型自动自动装配,Spring 容器查找 beans 的属性,这些 beans
    在 XML 配置文件中被设置为 byType。之后容器试图匹配和装配和该 bean 的属性类型一样的
    bean。如果有多个 bean 符合条件,则抛出错误。
    constructor:这个同 byType 类似,不过是应用于构造函数的参数。如果在 BeanFactory
    中不是恰好有一个 bean 与构造函数参数相同类型,则抛出一个严重的错误。
    autodetect:如果有默认的构造方法,通过 construct 的方式自动装配,否则使用
    byType 的方式自动装配。
    4.对 Java 接口代理模式的实现原理的理解?
    答:
    1、字符串拼凑出代理类
    2、用流的方式写到.Java 文件
    3、动态编译.Java 文件
    4、自定义类加载器把.class 文件加载到 jvm
    5、返回代理类实例
    其实核心就是根据接口反射出接口中的所有方法,然后通过拼凑或者 cglib 字节码的方
    式把代理类反射出来,而代理类在内存中就会对某种类型的类进行调用,invocationHandler
    (Jdk)或者 MethodInterceptor(cglib)

5.怎么理解面向切面编程的切面?
答:
切面指的是一类功能,比如事务,缓存,日志等。切面的作用是在目标对

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章