并发和JVM面试归纳:一

ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于一个小型的HashMap
  1. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
  2. 先生成新的数组,然后转移元素到新数组中
  1. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
  1. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
  2. ConcurrentHashMap是支持多个线程同时扩容的
  1. 扩容之前也先生成一个新的数组
  2. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

ThreadLocal的底层原理 

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  1. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
  2. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

图解:

 

 如何理解volatile关键字

       在并发领域中,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。

ReentrantLock中的公平锁和非公平锁的底层实现

       首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

       不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

       另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

图解:

公平锁枷锁:

非公平锁加锁: 

ReentrantLock中tryLock()和lock()方法的区别 

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

CountDownLatch和Semaphore的区别和底层原理 

       CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用                     CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。

       对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

        Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

 Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  1. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  2. 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Sychronized和ReentrantLock的区别 

  1. sychronized是一个关键字,ReentrantLock是一个类
  2. sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  1. sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
  2. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  1. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  2. sychronized底层有一个锁升级的过程

线程池的底层工作原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  1. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  1. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

 JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的

 

JVM中哪些可以作为gc root

 什么是gc root,JVM在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root

你们项目如何排查JVM问题 

对于还在正常运行的系统:

  1. 可以使用jmap来查看JVM中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
  1. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
  2. 通过各个命令的结果,或者jvisualvm等工具来进行分析
  1. 首先,初步猜测频繁发生fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
  2. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统:

  1. 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利用jsisualvm等工具来分析dump文件
  1. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
  2. 然后再进行详细的分析和调试

总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

说说类加载器双亲委派模型

JVM中存在三个默认的类加载器:

  1. BootstrapClassLoader
  2. ExtClassLoader
  1. AppClassLoader

AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader。

JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。

所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。

Guess you like

Origin blog.csdn.net/weixin_46300935/article/details/121940258