八股-Java并发

发布时间 2023-09-18 16:08:54作者: EnkiZhang
title: 八股--Java并发
top: false
cover: false
toc: true
mathjax: true
date: 2023-09-05 16:28:51
password:
summary:
tags:
categories:

启动线程的方式

线程继承 Thread 类 实现 Callable 或者 Runable
使用 start()在处理器中注册线程并执行 run 方法
若只执行 run 方法会相当于执行了 Main 函数的一个方法体 并没有启动真正意义的新的线程。

线程之间的通信

java 线程通根据内存共享(隐形不可见)和消息传递(显示的)来实现
Java 的内存中堆是共享的。

JMM

JMM 时 Java 内存模型一种规范,屏蔽了各种硬件和操作系统对内存访问的差异,从而在不同平台上达到一直的内存访问效果,同时也规定了线程和内存之间的关系。JMM 控制了 Java 线程之间的通信

线程通过 JMM 的通信:
线程 A 向其他线程发送消息,需要借助共享内存去实现通信,即线程 A 先将自身本地内存中的变量刷到共享内存中去,其他线程在从共享内存中去读取线程 A 更新过的共享变量。
所以 JMM 控制本地内存和共享变量实现了内存的可见性。

Java 源代码到执行过程实现:
编译器的重排序,指令的并行重排序,内存系统重排序,最终执行的指令排序

happens-before 原则

通过禁止一些会改变程序执行结果的指令重排序,要求前一个操作的结果对后一个操作可见,即使这两和两个操作并不在同一个线程中。规定了一个或者多个编译器重排序的规则不需要人为的去实现。

Volatile 是轻量级同步机制,

Volatile 是轻量级同步机制,保证可见性、不保证原子性、禁止指令重排
特性:
保证可见性:当变量被 Volatile 修饰后,当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
不保证原子性

禁止指令重排: 在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

Volatile 保证了可见性,当变量声明为 Volatile 那么 JMM 可以确保所有线程对该变量的值是一致的

说说乐观锁和悲观锁

悲观锁:认为每次对变量的访问都会引起对变量的修改,所以在每次的线程访问时都需要加锁,其他线程的操作只能等待。可以使用 synchronized 关键字或 ReentrantLock 类来实现悲观锁。适合于有高并发、高冲突的时候保证数据一致性。(数据库的行锁和表锁)

乐观锁:线程的每次访问只在提交对共享数据做检查,其他情况下并不担心出现共享的问题。 不会频繁的加锁适合于读多写少的场景,使用版本号或者 cas 实现

项目中使用锁的情况

synchronized

保证了原子性和可见性
Java 中的每一个对象都可以作为锁
锁的种类

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象
    synchronized 的锁存放于对象头中,其中的 Mark word 标记字段记录了锁的变化

锁升级过程

jdk1.6 之前的 synchronized 基于进入和退出 monitor 对象来同步代码块。这种锁称为重量级锁,为了提升获得锁和释放锁带来的性能增加了“偏向锁”和“轻量级锁”,锁随着竞争关系不断升级,同时这种锁升级却不能降级。

  1. 偏向锁
    当同步代码块中只有一个线程在访问,不存在竞争的情况下,会使用偏向锁来偏向于第一个访问锁的进程,从而达到减少锁释放和获取所消耗的性能。
    偏向锁获取过程如下:进程访问同步代码块获取锁时判断对象头中的标志位是否时偏向锁且线程 ID 是否指向当前线程。若是则直接执行同步代码,否则若线程 ID 未指向当前线程,通过 CAS 竞争,若竞争成功将 mark word 中的线程 Id 设置为当前线程之后指向同步代码块,若竞争失败,会在全局安全点即(在这个时间点上没有字节码正在执行)上对进程进行挂起,偏向锁升级为轻量级锁

    偏向锁的释放,只在其他线程尝试竞争时才会释放。适用于无竞争状态

  2. 当偏向锁有竞争时会升级为轻量级锁。当进程进入同步代码块中 JVM 在当前线程栈帧中创建存储锁记录的空间用于拷贝对象头的 mark word 之后使用 CAS 将对象头中的 mark word 替换为指向锁记录的指针,若成功则获得轻量级锁,若失败则表示有竞争继续 cas 自旋操作。

轻量级锁在释放时将锁记录中的 Mark word 替换会对象头,如果成功表示没有竞争 失败则表示存在竞争需要进行锁膨胀为重量级锁

CAS 自旋锁

若等待的线程能够在短时间内释放资源,线程等待其他线程释放锁避免了内核态和用户态切换带来的资源消耗。
但是自旋会消耗 cpu,当竞争非常激烈的情况下会产生不必要的资源浪费。因此避免无用的消耗当转为重量级锁就就不会再恢复到轻量级锁状态。其他线程在重量级锁状态下会阻塞。

竞争过程偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;(旋转的阈值在 1.6 之后引入了自适应阈值,并不是固定的)

ConcurrentHashMap

在并发编程中使用 HashMap 可能导致程序死循环。而使用线程安全的 HashTable 效率又非常低下,因此需要 ConcurrentHashMap。并发环境下 hashMap 会产生环形链造成死循环。而 hashTable 的同步方法会阻塞其他线程的访问进去等待状态,效率底下。

ConcurrentHashMap 使用了分段锁技术来提高 Hash table 中的访问问题,在写操作时通过使用分段锁技术对操作段加锁而不影响其他段的操作。

1.8 中的设计
初始化:
put 流程:
先计算 key 的 hash 值,
判断数组是否为空,空则进行初始化数组
根据 hash 值找到对应的数组并得到头节点,若数组为空则进行 CAS 操作放入元素,
若自旋失败表示有竞争,

key 为什么不能为 null

Java 的集合框架通常禁止键或值为 null,以确保编程规范的一致性。null 无法散列不能确定 hash 位置

线程池

好处:

  1. 降低资源消耗。通过重复是利用资源降低线程创建和销毁造成的消耗
  2. 提高响应速度。任务到达线程池中不需要等待即可执行任务
  3. 提高线程的可管理性。利用线程池对线程的分配和调优监控,做到合理的管理线程

线程池工作流程:
当提交过来一个线程后
线程池判断当前运行的线程数小于核心线程数,则启动一个新的线程处理惹任务
若核心线程数达到最大值后,任务会被放入等待队列中进行排队。
若等待队列达到最大值,此时判断线程池中的非核心线程数+核心线程数是否达到最大线程数量 若没达到则创建线程来处理等待队列中的任务。否则任务进入到拒绝策略中。
线程从等待队列中取任务执行,执行完毕后在超过非核心线程的最长等待时间会,且当前线程数超过核心线程数会被关闭。

创建线程池的方式

通过 Executors 创建
通过 ThreadPoolExecutor 手动创建线程池。

  • 线程池参数
    核心线程数
    最大线程数
    非核心线程数空闲时间
    时间单位
    等待队列
    线程工厂
    拒绝策略
  • 等待队列
    有界队列:
    • ArrayBlockingQueue 按照先进先出排序 数组实现
    • LinkedBlockingQueue 链表实现 可设置容量 不设置容量为无限队列容量 Integer.MAX_VALUE
    • DelayQueue 延迟队列 执行定时周期任务的队列
      无界队列
    • PriorityBlockingQueue 优先级队列
  • 拒绝策略
    AbortPolicy :直接抛出异常,默认使用此策略
    CallerRunsPolicy:用调用者所在的线程来执行任务,哪来回哪去
    DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务,把最前面的挤掉
    DiscardPolicy :当前任务直接丢弃。

线程池 submit 和 execute 方法的区别

submit 会返回线程执行的异步结果 future 对象 根据 future 对象可以判断是否执行成功
execute 用于提交不需要返回值的任务

线程池的关闭

shutdown() 将线程池状态置为 shutdown,并不会立即停止,任务不能继续提交,直到线程池内部处理完成才结束
shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了

创建线程池和手动创建线程区别在哪里?面试题

即创建线程池的好处

介绍一下锁,从 AQS 到 CLH,再到 ReentrantLock 和 synchronized

AQS 是一种抽象同步队列