java面经并发编程总结

在面试中并发编程是必然会被问到的一个问题,我结合自身面试经验和对大厂面试题分析,经过知识梳理,总结有以下几个部分。
1理解线程的状态转换、(理解这个是基础,有助于后续理解线程之前协作,线程池等)
2线程的同步与互斥。CAS、synchronized 和 lock。
3线程池的运作机制,实现方式,使用场景。
4以及 JUC 中常用的工具类。
5死锁
6线程通讯
7延伸各种锁机制的理解
深刻理解上述概念和实现原理,应对多线程不成问题,下面我们按个介绍。
1我们在理解线程的状态之前,先来说说神马是线程,线程又依托于进程存在,所以我这里先说进程,进程就是我们通常知道的一个应用,比如微信,qq等,拥有独立的内存区域;
如下图所示

image.png

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
现在说线程,线程与进程相似,但线程是一个比进程更小的执行单位。
举个例子 jvm是个进程 main就是运行在进程之中的线程;
理解了什么是线程,现在解释线程运行过程中有哪些状态,有NEW起始、RUNNABLE运行、BLOCKED阻塞、WAITING等待、TIMED_WAITING指定时间等待、TERMINATED 终止六种状态,这些状态对应 Thread.State 枚举类中的状态。
这里有个图可以展示线程之间的转换 和其对应的方法
image.png

ps 网图 能说明问题就行;
2了解了线程的状态,我们就可以理解线程的同步与互斥机制;
解决线程同步与互斥的主要方式是 CAS、synchronized 和 lock。
2.1 先来说一下什么是cas,即Conmpare And Swap 比较和交换,是乐观锁的一种实现,此种方式假设并发并不是时时刻刻存在,在读取数据时候不加锁,在写回数据的时候进行加锁,比较数据是否发生修改 如果发生修改就继续读取的操作。
2.1.1了解了实现方式,我们看看jdk,看看怎么实现,理论与实践相结合;
此种方式实现AtomicInteger为例
image.png

最后追踪是native boolean compareAndSwapInt 方法 源码如下;
http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/4fc084dac61e/src/share/vm/prims/unsafe.cpp
image.png

源码为c++;大致看一下最后cmpxchg 为汇编指令 比较交换操作数,此过程底层运用了操作系统原语实现的,保证不会中断。
但是此类操作会导致aba问题,就是一个方法修改了值,又有一个方法改回去了,比较并没有作用,
解决方案是加时间戳 判断。

2.2synchronized 原理
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。
Synchronized 作用范围
作用于方法时,锁住的是对象的实例(this);
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen
(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,
会锁所有调用该方法的线程;
synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,
当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中
Synchronized 核心组件

  1. Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
  2. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  3. Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
  5. Owner:当前已经获取到所资源的线程被称为 Owner;
  6. !Owner:当前释放锁的线程
    synchronized 实现


    image.png
  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
    ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将
    一部分线程移动到 EntryList 中作为候选竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定
    EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
    OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
    JVM 中,也把这种选择行为称之为“竞争切换”。
  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList
    中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify
    或者 notifyAll 唤醒,会重新进去 EntryList 中。
  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
    来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先
    尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是
    不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
    资源。
    参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
    上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
    程加锁消耗的时间比有用操作消耗的时间更多。
  9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向
    锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做
    了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

2.3lock 原理
在介绍 Lock 前,先介绍 AQS(抽象的队列同步器),也就是队列同步器,这是实现 Lock 的基础。
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问
共享资源的同步器框架,许多同步类实现都依赖于它,如常用的
ReentrantLock/Semaphore/CountDownLatch。

image.png

它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被
阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的
访问方式有三种:
getState()
setState()
compareAndSetState()
AQS 定义两种资源共享方式
Exclusive 独占资源-ReentrantLock
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
Share 共享资源-Semaphore/CountDownLatch
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS 这里只定义了一个
接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成
abstract ,是 因 为独 占模 式 下 只 用实现 tryAcquire-tryRelease ,而 共享 模 式 下 只用 实 现
tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模
式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实
现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/
唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
1.isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
2.tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。 3.tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。 4.tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余
可用资源;正数表示成功,且有剩余资源。
5.tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回
true,否则返回 false。
同步器的实现是 ABS 核心,以 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 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程
就会从 await()函数返回,继续后余动作。
ReentrantReadWriteLock 实现独占和共享两种方式
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire�tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器
同时实现独占和共享两种方式,如 ReentrantReadWriteLock。

3线程池的运作机制,使用场景。
Executors 工具类中提供了 5 种类型
newCachedThreadPool很多短期异步任务的程序而言,这些线程池通常可提高程序性能
newFixedThreadPool固定大小线程池,特点是线程数固定,使用无界队列,适用于任务数量不均匀的场景、对内存压力不敏感但系统负载比较敏感的场景;
newScheduledThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
newSingleThreadExecutor Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
newWorkstealingPoll 工作窃取线程池,使用的是 ForkJoinPool,是固定并行度的多任务队列,适合任务执行时长不均匀的场景。
线程池的工作原理
不想画了 网上找的哈哈~


image.png

1execute 和 submit时判断是否达到核心线程数 没有达到就创建线程执行
2如果达到核心线程数量,就看缓冲队列是否满,没有满 放入队列 等待线程空闲时间执行对列任务
3如果对列已经满了 则判断是否达到了最大线程数,没有达到 创建线程执行任务
4如果达到了最大线程的数量 就执行拒绝策略
4 JUC 中常用的工具类。
4.1首先是基本数据类型的元子类Atomic* 都是基于cas的实现
LongAdder 、doubleAdder 都是基于分段锁的思想;属于空间换时间,更适用于高并发场景
AtomicStampedReference 和 AtomicMarkableReference 用于解决前面提到的 ABA 问题,分别基于时间戳和标记位来解决问题。
4.2 锁相关的类
与 ReentrantLock 的独占锁不同,Semaphore 是共享锁,
其他的后续补充
异步执行的相关类
CompletableFuture可以支持流式调用,可以方便的进行多 future 的组合使用,例如可以同时执行两个异步任务,然后对执行结果进行合并处理。还可以很方便地设置完成时间。
ForkJoinPool,采用分治思想,将大任务分解成多个小任务处理,然后在合并处理结果。ForkJoinPool 的特点是使用工作窃取算法,可以有效平衡多任务时间长短不一的场景。
4.3 其他的工具类
LinkedBlockingDeque
是一个双端队列 分别可以从对头 队尾出队
ArrayBlockingQueue
单端队列 队尾入队 对头出队
CountDownLatch
计数器功能 等多个线程执行结束进行汇总
CyclicBarrier
等待一组线程到某个状态以后 一起执行,可以进行并发测试
Semaphore
用来控制对共享资源的访问并发度
ConcurrentHashMap
是一个线程安全的map 1.7是基于分段锁的思想实现

5死锁
锁死的定义:多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
死锁如何排查、
jps + jstack
https://www.cnblogs.com/aflyun/p/9194104.html
线程安全和内存模型的关系
https://www.cnblogs.com/haoworld/p/java-bing-fa-xian-cheng-an-quan-he-nei-cun-mo-xing.html
6线程通讯
sleep 和 wait
sleep不释放锁 wait释放锁
wait 和 notify
wait线程阻塞休眠 notify 唤醒休眠的线程
notify 和 notifyAll
如果线程a获取对象锁,并且wait 对象 notify可以唤醒对应的线程.notifyall是唤醒所有休眠的线程

7延伸各种锁机制的理解
偏向锁、轻量级锁、重量级锁、锁优化、锁消除、锁粗化、自旋锁、可重入锁、阻塞锁、死锁
偏向锁、
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
重量级锁
当使用synchronized时线程先使用偏向锁 ,如果有竞争就是轻量级锁,在获取不到就会进行短暂的自旋,自旋获取不到就会升级为重量级锁
锁优化
以上介绍的锁不是我们代码中能够控制的,但是借鉴上面的思想,我们可以优化我们自己线程的加锁操作;
锁优化1 减少锁的时间
不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
锁优化2 减少锁的粒度
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;
java中很多数据结构都是采用这种方法提高并发操作的效率:ConcurrentHashMap java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组
锁粗化
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
在以下场景下需要粗化锁的粒度:
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
自旋锁
通过自旋进行锁的获取 cas
可重入锁
同一个线程可以再次获取锁ReentrantLock等
阻塞锁、
多个线程同时调用同一个方法的时候,所有线程都被排队处理了。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。 wait 等
死锁上面介绍过了
锁消除
锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。

你可能感兴趣的:(java面经并发编程总结)