3万5千字70+道大厂Java经典面试题【金三银四(金九银十)面试小抄之Java经典多线程与高并发篇总结】(附答案)

在这里插入图片描述

作者简介:硕风和炜,CSDN-Java领域新星创作者,保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享
座右铭:人生如棋,我愿为卒,行动虽慢,可谁曾见我后退一步?
关注我,私信回复面试题 ,获取《3万5千字70+道大厂多线程与高并发面试题【金三银四(金九银十)面试小抄之Java经典多线程与高并发篇总结】(附答案)》pdf

目录

  • 一.前言
  • 二.Java经典面试题之多线程与高并发专题篇
    • 1.为什么要使用并发编程?
    • 2.并发编程有什么缺点?
    • 3.并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
    • 4.出现线程安全问题的原因以及解决办法
    • 5.出现线程安全问题的解决办法
    • 6.什么是线程和进程?
    • 7.进程与线程的区别
    • 8.Happens-Before规则是什么?
    • 9.进程间的通信方式?
    • 10.什么是线程死锁?
    • 11.死锁的四个必要条件是什么?
    • 12.如何避免线程死锁?
    • 13.介绍一下线程的生命周期及状态?
    • 14.创建线程有哪几种方式?
    • 15.线程的 run()和 start()有什么区别?
    • 16.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
    • 17. 什么是 Callable 和 Future?
    • 18.什么是 FutureTask
    • 19.线程安全活跃态问题?
    • 20.sleep() 和 wait() 有什么区别?
    • 21.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
    • 22.为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
    • 23.线程的 sleep()方法和 yield()方法有什么区别?
    • 24.Java 如何实现多线程之间的通讯和协作?
    • 25.守护线程和用户线程有什么区别呢?
    • 26.程序开多少线程合适?
    • 27.ABA问题遇到过吗,详细说一下?
    • 28. volatile的可见性和禁止指令重排序怎么实现的?
    • 29.as-if-srial规则和happens-before规则的区别
    • 30.synchronized 和 volatile 的区别是什么?
    • 31.synchronized 和 Lock 有什么区别?
    • 32.synchronized 和 ReentrantLock 区别是什么?
    • 33. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
    • 34.synchronized可重入的原理
    • 35.线程 B 怎么知道线程 A 修改了变量
    • 36.多线程中 synchronized 锁升级的原理是什么?
    • 37.什么是自旋?
    • 38.ThreadLocal的原理是什么,使用场景有哪些?
    • 39.ThreadLocal有哪些内存泄露问题,如何避免?
    • 40. Lock 接口(Lock interface)是什么?对比同步它有什么优势?
    • 41.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
    • 42.什么是 CAS
    • 43.CAS 的会产生什么问题?
    • 44.Java中怎么防止死锁?
    • 45.死锁与活锁的区别,死锁与饥饿的区别?
    • 46.AQS 介绍
    • 47.AQS 原理分析
    • 48.ReentrantLock(重入锁)实现原理与公平锁非公平锁区别什么是可重入锁(ReentrantLock)?
    • 49. ReadWriteLock 是什么?
    • 50.什么是ConcurrentHashMap?
    • 51.Java 中 ConcurrentHashMap 的并发度是什么?
    • 52.什么是并发容器的实现?
    • 53.Java 中的同步集合与并发集合有什么区别?
    • 54.SynchronizedMap 和 ConcurrentHashMap 有什么区别?
    • 55.### CopyOnWriteArrayList 是什么,可以用于什么应用场景?有哪些优缺点?
    • 56.什么是阻塞队列?阻塞队列的实现原理是什么?
    • 57.Executors类创建四种常见线程池什么是线程池?有哪几种创建方式?
    • 58.线程池有什么优点?
    • 59.线程池都有哪些状态?
    • 60.什么是 Executor 框架?为什么使用 Executor 框架?
    • 61.在 Java 中 Executor 和 Executors 的区别?
    • 62.线程池中 submit() 和 execute() 方法有什么区别?
    • 63.什么是线程组,为什么在 Java 中不推荐使用?
    • 64.Executors和ThreaPoolExecutor创建线程池的区别
    • 65.ThreadPoolExecutor构造函数重要参数分析
    • 66.什么是原子操作?在 Java Concurrency API 中有哪些原子类 (atomic classes)?
    • 67.在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
    • 68.Semaphore 有什么作用
    • 69.什么是线程间交换数据的工具Exchanger
    • 70.常用的并发工具类有哪些?
    • 71. 还在更新完善中,敬请期待
  • 三.下节预告
  • 四.共勉

一.前言

     之前的一篇文章帮大家总结了一篇关于Java基础篇的面试文章,大家可以直接进行跳转学习,地址附到了下面,大家自行收藏学习。

6万字144道耗时72小时吐血整理【金三银四(金九银十)面试小抄之Java经典面试题基础篇总结】(附答案)
2万字8千字72道大厂JVM面试题【金三银四(金九银十)面试小抄之Java经典面试题JVM篇总结】(附答案)

二.Java经典面试题之多线程与高并发专题篇

1.为什么要使用并发编程?

  1. 通过并发编程的形式可以将多核CPU 的计算能力发挥到极致,性能得到提升;
  2. 随着技术的进步与发展,现在的系统要求扛住百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

2.并发编程有什么缺点?

  1. 内存泄漏问题
  2. 上下文切换,占据CPU,效率低
  3. 线程安全问题
  4. 死锁问题
  5. 。。。

3.并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?

  1. 原子性:原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

  2. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)

  3. 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

4.出现线程安全问题的原因以及解决办法

  1. 线程切换带来的原子性问题
  2. 缓存导致的可见性问题
  3. 编译优化带来的有序性问题

5.出现线程安全问题的解决办法

  1. JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  2. synchronized、volatile、LOCK,可以解决可见性问题
  3. Happens-Before 规则可以解决有序性问题

6.什么是线程和进程?

  1. 进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的程序就是一个进程。

  2. 线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

7.进程与线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process), 它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。 具体区别如下:

  1. 调度:在多线程OS中,线程是独立调度的基本单位,进程是资源分配的基本单位;
  2. 拥有资源:进程拥有资源,而线程(一般)不拥有资源,这样做是为了减少切换线程的开销;
  3. 并发性: 在多线程OS中,进程可以并发执行,线程也可以并发执行,线程的并发大大提高了OS的并发性;
  4. 系统开销:创建和撤销进程,需要分配和回收相关资源,开销较大;切换进程的成本较高,相对而言,创建和切换线程的成本较低,只需要保存少量寄存器内容,开销小;
  5. 地址空间和其它资源:进程间的地址空间相互独立,同一进程的线程间共享进程资源,进程间的线程彼此不可见;
  6. 通信方面:进程通信需要同步或互斥手段的辅助;线程间可以直接读写数据段(如全局变量)来通信。

8.Happens-Before规则是什么?

  1. 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  2. 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile规则:对一个volatile变量的写,happens-before于任意后续对一个volatile变量的读。
  4. 传递性:若果A happens-before B,B happens-before C,那么A happens-before C。
  5. 线程启动规则:Thread对象的start()方法,happens-before于这个线程的任意后续操作。
  6. 线程终止规则:线程中的任意操作,happens-before于该线程的终止监测。我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  7. 线程中断操作:对线程interrupt()方法的调用,happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到线程是否有中断发生。
  8. 对象终结规则:一个对象的初始化完成,happens-before于这个对象的finalize()方法的开始。

9.进程间的通信方式?

  1. 共享存储(Shared-memory):(1) 基于共享数据结构的通信方式。 (2) 基于共享存储区的通信方式。
  2. 消息传递(message-passing):通过发消息和收消息两个原语进行操作
  3. 管道通信(Pipe):基于文件系统和缓冲思想实现的一种通信方式,管道以先进先出(FIFO)方式组织数据传输,管道是一个单向通信信道,如果进程间要进行双向通信,通常需要定义两个管道,管道通过系统调用read(), write()函数进行读写操作。管道通信包括匿名管道(pipe)【只能亲缘关系进程通信】和命名管道【可以实现本机任意两个进程通信】。
  4. 信号:信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  5. 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
  6. Socket通信:可用于不同机器间的进程通信,任何进程间都能通讯,但速度慢。

10.什么是线程死锁?

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资 源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推 进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进 程(线程)称为死锁进程(线程)。

11.死锁的四个必要条件是什么?

  1. 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只 能被一个线程(进程)占用,直到被该线程(进程)释放
  2. 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对 已获得的资源保持不放。
  3. 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程 强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环 路(类似于死循环),造成永久阻塞

12.如何避免线程死锁?

只需要破坏产生死锁的四个条件中的其中一个就可以了。

  1. 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

  2. 破坏请求与保持条件:我们可以一次性申请所有的资源。

  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  4. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

13.介绍一下线程的生命周期及状态?

3万5千字70+道大厂Java经典面试题【金三银四(金九银十)面试小抄之Java经典多线程与高并发篇总结】(附答案)_第1张图片

1.创建
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
2.就绪
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
3.运行
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。添加到了线程列表中,如果OS调度选中了,就会进入到运行状态
4.阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:

  • 1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  • 2、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 3、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)。
  • 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
  • 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
  • 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  • 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  • 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
  • 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

5.死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

14.创建线程有哪几种方式?

  1. 继承 Thread 类;
  2. 实现 Runnable 接口;
  3. 实现 Callable 接口;
  4. 使用 Executors 工具类创建线程池继承 Thread 类

15.线程的 run()和 start()有什么区别?

  1. 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的, run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
  2. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。
  3. run() 可以重复调用,而 start() 只能调用一次。
  4. start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待 run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
  5. run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

16.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

start():new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

run():而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

17. 什么是 Callable 和 Future?

  1. Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
  2. Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。

18.什么是 FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个

Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判

断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了

Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

19.线程安全活跃态问题?

线程安全的活跃性问题可以分为 死锁、活锁、饥饿

  1. 活锁就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试
    1). 我们开发中使用的异步消息队列就有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。
    2). 解决活锁的方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试
  2. 饥饿就是线程因无法访问所需资源而无法执行下去的情况
    1. 饥饿分为两种情况:
      1). 一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态
      2). 另一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行
    2. 解决饥饿的问题有几种方案:
      1). 保证资源充足,很多场景下,资源的稀缺性无法解决
      2). 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源
      3). 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短
  3. 死锁线程在对同一把锁进行竞争的时候,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁

20.sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行

  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

21.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

  1. Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

  2. wait(), notify()和 notifyAll()这些方法在同步代码块中调用

  3. 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。

  4. 综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。

22.为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

23.线程的 sleep()方法和 yield()方法有什么区别?

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
  4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

24.Java 如何实现多线程之间的通讯和协作?

Java中线程通信协作的 常见的两种方式:

  1. syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

  2. ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll() 线程间直接的数据交换:

25.守护线程和用户线程有什么区别呢?

守护线程和用户线程

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网 络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护 线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程 会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部 同时还启动了好多守护线程,比如垃圾回收线程。 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线 程运行。而守护线程不会影响 JVM 的退出。

注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算 逻辑
  4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清 理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守 护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语 句块可能无法被执行。

26.程序开多少线程合适?

  1. CPU 密集型程序,一个完整请求,I/O操作可以在很短时间内完成,CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0
    1. 单核CPU: 一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0。单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程。
    2. 多核 : 如果是多核CPU 处理 CPU 密集型程序,我们完全可以最大化的利用 CPU 核心数,应用并发编程来提高效率。CPU 密集型程序的最佳线程数就是:理论上线程数量 = CPU 核数(逻辑),但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1(经验值),计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作
  2. I/O 密集型程序,与 CPU 密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说 I/O 操作占比很大部分,等待时间较长,线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程
    1. I/O 密集型程序的最佳线程数就是: 最佳线程数 = CPU核心数 (1/CPU利用率) = CPU核心数 (1 + (I/O耗时/CPU耗时))
    2. 如果几乎全是 I/O耗时,那么CPU耗时就无限趋近于0,所以纯理论你就可以说是 2N(N=CPU核数),当然也有说 2N + 1的,1应该是backup
    3. 一般我们说 2N + 1 就即可

27.ABA问题遇到过吗,详细说一下?

  1. 有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成B。
  2. 首先线程1获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新(CompareAndSwap),成功将变量的值从A更新成B。
  3. 更新完毕之后,恰好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。
  4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后有更新成了B。但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。

28. volatile的可见性和禁止指令重排序怎么实现的?

  • 可见性:
    volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性
    写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。
  • 禁止指令重排序:
    volatile是通过内存屏障来禁止指令重排序
    JMM内存屏障的策略
    • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
    • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
    • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
    • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

29.as-if-srial规则和happens-before规则的区别

  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens before关系保证正确同步的多线程程序的执行结果不被改变。 2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按 happens-before指定的顺序来执行的。
  2. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

30.synchronized 和 volatile 的区别是什么?

synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。

volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别

  • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized关键字要好。但是volatile关键字只能用于变量而

synchronized关键字可以修饰方法以及代码块。synchronized关键字在

JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。

31.synchronized 和 Lock 有什么区别?

  • 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

更详细的区别如图所示:

区别类型 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是JVM的一个接口
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放 1、以获取锁的线程执行完同步代码,释放锁2、线程执行发生异常,jvm会让线程释放 在finally中必须释放锁,不然容易造成线程死锁
锁类型 锁可重入、不可中断、非公平 可重入、可判断 可公平(两者皆可)
性能 少量同步 适用于大量同步
支持锁的场景 1. 独占锁 1. 公平锁与非公平锁

32.synchronized 和 ReentrantLock 区别是什么?

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比

synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都

相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

相同点:两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0 时才能释放锁。主要区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
  • 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word
  • Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

33. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

synchronized关键字最主要的三种使用方式:

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

  2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

  3. 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” 双重校验锁实现对象单例(线程安全)

public class Singleton {

	private volatile static Singleton uniqueInstance;
	
	private Singleton() {}
	
	public static Singleton getUniqueInstance() {
		//先判断对象是否已经实例过,没有实例化过才进入加锁代码
		if (uniqueInstance == null) {
			//类对象加锁
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
				uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间

  2. 初始化 uniqueInstance

  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。说一下 synchronized 底层实现原理?synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。 synchronized 同步语句块的情况

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

通过JDK 反汇编指令

 javap -c -v SynchronizedDemo

可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是

monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此 后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。

仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。

34.synchronized可重入的原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

35.线程 B 怎么知道线程 A 修改了变量

  1. volatile 修饰变量
  2. synchronized 修饰修改变量的方法
  3. wait/notify
  4. while 轮询

36.多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果

一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

37.什么是自旋?

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

38.ThreadLocal的原理是什么,使用场景有哪些?

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}
    
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的载体,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

39.ThreadLocal有哪些内存泄露问题,如何避免?

每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OqE4BPQ-1679582912944)(images/threadlocal.png)]由于存在这个强引用关系,会导致value无法回收。如果这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏情况。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那就更坑了。 那么要怎么避免这个问题呢? 在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。 下面再对ThreadLocal进行简单的总结:

  • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
  • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
  • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。

40. Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

(1) 可以使锁更公平

(2) 可以使线程在等待锁的时候响应中断

(3) 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

(4) 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

41.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在

Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:

  1. 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

  2. java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值

(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

42.什么是 CAS

  1. CAS 是 compare and swap 的缩写,即我们所说的比较交换。cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

  2. CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。

  3. CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

  4. java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

43.CAS 的会产生什么问题?

  1. ABA 问题:比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
  2. 循环时间长开销大:
    对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized
  3. 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。什么是死锁?当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

44.Java中怎么防止死锁?

产生死锁的必要条件:

  1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以 大可能地避免、预防和 解除死锁。

防止死锁可以采用以下的方法:

  • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、
  • ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
  • 尽量减少同步的代码块。

45.死锁与活锁的区别,死锁与饥饿的区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。

  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

46.AQS 介绍

  1. AQS的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks包下面。
  2. AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

47.AQS 原理分析

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制

AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过protected类型的getState,setState,compareAndSetState进行操作

1	//返回同步状态的当前值
2	protected final int getState() {
3	return state;
4	}
5	// 设置同步状态的值
6	protected final void setState(int newState) {
7	state = newState;
8	}
9	//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect (期望值)
10	protected final boolean compareAndSetState(int expect, int update) { 
11 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
12 }

AQS 对资源的共享方式

AQS定义两种资源共享方式

  • xclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

    非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

AQS底层使用了模板方法模式同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样

(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)

  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回fals e。

默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

48.ReentrantLock(重入锁)实现原理与公平锁非公平锁区别什么是可重入锁(ReentrantLock)?

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。

重入性的实现原理要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

49. ReadWriteLock 是什么?

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。

ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离

技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

而读写锁有以下三个重要的特性:

(1) 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2) 重进入:读锁和写锁都支持线程重进入。

(3) 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

50.什么是ConcurrentHashMap?

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。

那么它到底是如何实现线程安全的?

JDK 1.6版本关键要素:

  • segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了

CAS + synchronized 来保证并发安全性。

51.Java 中 ConcurrentHashMap 的并发度是什么?

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数

的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。

在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。

52.什么是并发容器的实现?

何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操

作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

53.Java 中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像

ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

54.SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。

这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。

另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出

ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

55.### CopyOnWriteArrayList 是什么,可以用于什么应用场景?有哪些优缺点?

CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

CopyOnWriteArrayList 的使用场景通过源码分析,我们看出它的优缺点比较明显,所以使用场景也就比较明显。就是合适读多写少的场景。

CopyOnWriteArrayList 的缺点

  1. 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。

  2. 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然

CopyOnWriteArrayList 能做到 终一致性,但是还是没法满足实时性要求。

  1. 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList 的设计思想

  1. 读写分离,读和写分开

  2. 终一致性

  3. 使用另外开辟空间的思路,来解决并发冲突并发容器之ThreadLocal详解

56.什么是阻塞队列?阻塞队列的实现原理是什么?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。分别是:

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。 PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用 经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

57.Executors类创建四种常见线程池什么是线程池?有哪几种创建方式?

池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类

Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

(1) newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

(2) newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的 大大小。线程池的大小一旦达到 大值就

会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的 大线程大小。

(4) newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

58.线程池有什么优点?

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制 大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

59.线程池都有哪些状态?

  • RUNNING:这是 正常的状态,接受新的任务,处理等待队列中的任务。 SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • ​ TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为
  • TIDYING 状态时,会执行钩子方法 terminated()。
  • ​ TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

60.什么是 Executor 框架?为什么使用 Executor 框架?

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。

所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以

回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。

61.在 Java 中 Executor 和 Executors 的区别?

  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
  • Executor 接口对象能执行我们的线程任务。
  • ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用 ThreadPoolExecutor 可以创建自定义线程池。
  • Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

62.线程池中 submit() 和 execute() 方法有什么区别?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有异常处理:submit()方便Exception处理

63.什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

64.Executors和ThreaPoolExecutor创建线程池的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 各个方法的弊端:

  • newFixedThreadPool 和 newSingleThreadExecutor:
    主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数 大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定

65.ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor3 个最重要的参数:

corePoolSize :核心线程数,线程数定义了 小可以同时运行的线程数量。

​ maximumPoolSize :线程池中允许存在的工作线程的 大数量

workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;

  2. unit :keepAliveTime 参数的时间单位。

  3. threadFactory:为线程池提供创建新线程的线程工厂

  4. handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略 ThreadPoolExecutor饱和策略

ThreadPoolExecutor饱和策略定义:

如果当前同时运行的线程数量达到 大线程数量并且队列也已经被放满了任时,

ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃 早的未处理的任务请求。

举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定

RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是

ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用

ThreadPoolExecutor.CallerRunsPolicy。当 大池被填满时,此策略为我们提供可

伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

66.什么是原子操作?在 Java Concurrency API 中有哪些原子类 (atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持CAS 的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。

为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

AtomicInteger 类的部分源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe(); 3 private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
		} catch (Exception ex) { throw new Error(ex);
		}
	}

	private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。

UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的 新值。

67.在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
  • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
  • CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入 barrierAction,指定当所有线程都到达时执行的业务功能;
  • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

68.Semaphore 有什么作用

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。

Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

Semaphore(信号量)-允许多个线程同时访问: synchronized 和

ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。

69.什么是线程间交换数据的工具Exchanger

Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供

了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过 exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

70.常用的并发工具类有哪些?

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到 后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉
  • CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

71. 还在更新完善中,敬请期待

关于多线程与高兵发更多的知识还在不断的完善学习中,大家敬请期待。

三.下节预告

都看到这里了,不点个赞是不是都不好意思走了,哈哈,那就赶快点个赞收藏起来我,创作不易,感谢关注!
如果关于JVM的面试知识你已经很好的掌握了,那么接下来给你安排的就是【2023金三银四面试小抄之Spring专题篇】,敬请期待!

四.共勉

最后,我想送给大家一句一直激励我的座右铭,希望可以与大家共勉!

在这里插入图片描述

你可能感兴趣的:(面试小抄,多线程与高并发,java,面试,多线程,高并发,锁)