面试总结

  1. 线程池的创建各个参数含义
    1. corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

    1. maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize

    1. keepAliveTime

线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于 corePoolSize 时才有用

    1. TimeUnit

keepAliveTime 的时间单位

    1. workQueue

workQueue 必须是 BlockingQueue 阻塞队列。当线程池中的线程数超过它的corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。通过 workQueue,线程池实现了阻塞功能workQueue用于保存等待执行的任务的阻塞队列,一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。

1)当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,

因此线程池中的线程数不会超过 corePoolSize。

2)由于 1,使用无界队列时 maximumPoolSize 将是一个无效参数。

3)由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。

4)更重要的,使用无界 queue 可能会耗尽系统资源,有界队列则有助于防

止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。所以我们一般会使用,ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。

    1. threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。Executors 静态工厂里默认的 threadFactory,线程的命名规则是“pool-数字-thread-数字”。

    1. RejectedExecutionHandler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:

(1)AbortPolicy:直接抛出异常,默认策略;

(2)CallerRunsPolicy:用调用者所在的线程来执行任务;

(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

(4)DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

  1. 举例说出最常用的两种线程池
    1. ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
    2. ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更强大
  2. Volatile关键字的底层实现原理(Volatile适用于一写多读的应用场景)
  1. 通过内存屏障,防止指令重排,从而达到可见性
  2. 底层是通过lock前缀指令实现的,它会锁定该内存区域的缓存(缓存行锁定),并回写到主内存。这个回写操作(缓存一致性协议)会使其他CPU里缓存了该内存地址的数据失效。
  1. 多线程间的通信
    1. 本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行
    2. while轮询的方式
    3. wait/notify机制
    4. 管道通信
  2. Synchonized关键字的使用和原理

使用:

    1. 用在方法上,或者是this,都是锁住的是当前对象,都只对单例有用
    2. (类型)用在静态代码块、或者是锁着class,跟对象有多少个实例无关
    3. 用在对象o上,只有拿到o的锁才可以进行方法的执行

每种锁之间互不影响

原理:synchronized 同步语句块的实现,使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁,也就是获取 monitor ( monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,这也是为什么 Java 中任意对象都可以作为锁的原因) 的持有权。当计数器为0,则可以成功获取,获取后将锁计数器设为1,也就是加1;相应的,在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  1. JVM的垃圾回收算法
    1. 复制算法
    2. 标记- 清除算法
    3. 标记- 整理算法
  2. 垃圾回收器
    1. Parallel Scavenge(新生代垃圾回收器)吞吐量优先收集器(复制算法),Parallel Old(老年代垃圾回收器,标记整理算法)
    2. Concurrent Mark Sweep  (CMS)一种以获取最短回收停顿时间为目标的收集器
      1. 初始标记-短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
      2. 并发标记-和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
      3. 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
      4. 并发清除:由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

CPU  敏感:CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。

浮动垃圾 :由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%)如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片

总体来说,CMS 是 JVM 推出了第一款并发垃圾收集器,所以还是非常有代表性。

但是最大的问题是 CMS 采用了标记清除算法,所以会有内存碎片,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个

参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。

这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡。

    1. Garbage First(G1)

设计思想

随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,但是如果按照传统的分代模型,总跳不出 STW 时间不可预测这点。

为了实现 STW 的时间可预测,首先要有一个思想上的改变。G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region

Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过

了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次

幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous

Region 作为老年代的一部分来进行看待。

初始标记( Initial Marking)

仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。

这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。

TAMS 是什么?

要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的分配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针,

从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

并发标记( Concurrent Marking)

从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫

描完成以后,并发时有引用变动的对象,这些对象会漏标( 后续再讲三色标记的时候会细讲这个问题),漏标的对象会被一个叫做

SATB(snapshot-at-the-beginning)算法来解决(这个下节课会细讲)

最终标记( Final Marking)

对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录(漏标对象)。

筛选回收( Live Data Counting and Evacuation)

负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构

成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,

是必须暂停用户线程,由多条收集器线程并行完成的。

特点

并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器

原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式

去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复

制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运

行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

追求停顿时间:

-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。

怎么玩?

该垃圾回收器适合回收堆空间上百 G。一般在 G1 和 CMS 中间选择的话平衡点在 6~8G,只有内存比较大 G1 才能发挥优势。

  1. CAS原子操作
    1. 是基于处理器的CAS指令的:每一个cas操作都包含三个运算符:一个内存地址V,一个期望值A,一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值 A,则将地址上的值赋为新值 B,否则不做任何操作
    2. CAS 是怎么实现线程的安全呢?语言层面不做处理,我们将其交给硬件—CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性即可实现基于原子操作的线程安全。
  2. mysql的事物的特性
    1.   原子性(atomicity ):一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不可能只执行其中的一部分操作
    2. 一致性(consistency ):一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏

例如:我给我老公发放生活费,我转账给我老公1000元,我老公的账户+1000元,我的账户-1000元,相加为0

    1. 持久性(durability ):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失
    2. 隔离性(isolation ):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。(对数据库的并行执行,应该像串行执行一样)
  1. 事物的隔离级别
    1.  未提交读(READ UNCOMMITED)脏读
    2.  已提交读 (READ COMMITED)不可重复读
    3.  可重复读(REPEATABLE READ)
    4.  可串行化(SERIALIZABLE)
  2. 脏读、幻读、不可重复读
    1. 脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
    2. 不可重复读:事务 A多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
    3. 幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不

可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

  1. 创建线程的几种方式
    1. Thread--------------对线程的抽象
    2. Runnable---------------对方法的抽象
    3. Callable------------------有返回值的创建线程的方式
    4. Executor--------------------线程池的方式
  2. 线程的几种状态
    1. 当线程继承Thread或者实现了Runnable创建了线程对象后,当new线程对象过后线程就进入了初始的状态。
    2. 当线程对象调用了start()方法的时候,线程启动进入可运行的状态,进入到就绪队列,等待分配cpu
    3. 拿到cup进入运行状态(如果需要竞争锁资源或者调用wait方法进去等待状态,调用yiled方法让出cpu资源进去就绪队列重新争夺CPU,调用notify后进去就绪队列重新争夺CPU)
  3. hashMap的key可以为空吗,怎么存储的。

可以为空,看源码,第一步就是拿到key进行hash运算,判断如果key为null,直接返回0。

解决hash碰撞的底层用的数组、链表加红黑树的方式实现的,当链表的长度大于8时,演变成红黑树,当红黑树的元素小于6时,有演变成链表

  1. Hashmap底层的红黑树和链表相互装换的阈值为什么要设为6和8呢?

首先出结论:和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。

红黑树中的TreeNode是链表中的Node所占空间的2倍,虽然红黑树的查找效率为o(logN),要优于链表的o(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。固,要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。

之所以是8,是因为Java的源码贡献者在进行大量实验发现,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此时的链表性能已经已经很差了,操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,在这种极端的情况下才会把链表转换为红黑树,链表转换为红黑树也是需要消耗性能的,为了挽回性能,权衡之下,才使用红黑树,提高性能的,大部分情况下hashMap还是使用链表

红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。中间有个差值7可以防止链表和树之间的频繁转换,假设一下:如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果HashMap不停的插入,删除元素,链表个数在8左右徘徊,就会频繁的发生红黑树转链表,链表转红黑树,效率会很低下。

  1. threadLocal的数据结构
  2. StringBuffer和StringBuild的区别
    1. StringBuffer线程安全的,效率比较低,使用的是syn内置锁来实现线程安全
    2. StringBuild线程不安全的,效率高,在非并发下使用StringBuild
  3. Set和list的区别

(1)重复对象

list方法可以允许重复的对象,而set方法不允许重复对象

(2)null元素

list可以插入多个null元素,而set只允许插入一个null元素

(3)容器是否有序

list是一个有序的容器,保持了每个元素的插入顺序。即输出顺序就是输入顺序,而set方法是无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序

  1. Mysql主键生成的几种方式
    1. 利用uuid函数生成唯一且不规则的主键id`id` varchar(100) COLLATE utf8_estonian_ci NOT NULL COMMENT '唯一不重复',
    2. id实现自动增长:`id` bigint(100) NOT NULL AUTO_INCREMENT COMMENT '唯一不重复',
  2. 多线程下载一个文件
  3. 线程池的使用场景
  4. 怎么实现一个公用的id生成器
  5. Eureka怎么做到平缓下线
  6. Mysql的架构
  7. Redis的架构
  8. Cup满了怎么进行排查
    1. 查消耗cpu最高的进程PID执行命令

    2. 执行top -c ,显示进程运行信息列表。按下P,进程按照cpu使用率排序

    3. 如下图所示,PID为3033的进程耗费cpu最高

      根据PID查出消耗cpu最高的线程号

      执行命令

    4. top -Hp 3033 ,显示一个进程的线程运行信息列表。按下P,进程按照cpu使用率排序

    5. 如下图所示,PID为3034的线程耗费cpu最高

      这是十进制的数据,转成十六进制为0xbda

      根据线程号查出对应的java线程,进行处理

      执行命令,导出进程快照

      jstack -l 3033 > ./3033.stack
      

      然后执行,grep命令,看线程0xbda做了什么

      cat 3033.stack |grep 'bda' -C 8
      

      输出如下

      至此定位到问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值