面试知识点准备与总结——(并发篇)

news2024/11/15 21:46:33

目录

    • 线程有哪些状态
    • 线程池的核心参数
    • sleep和wait的区别
    • lock 与 synchronized 的异同
    • volatile能否保证线程安全
    • 悲观锁和乐观锁的区别
    • Hashtable 与 ConcurrentHashMap 的区别
    • ConcurrentHashMap1.7和1.8的区别
    • ThreadLocal的理解
    • ThreadLocalMap中的key为何要设置为弱引用

线程有哪些状态

Java线程分为六种状态,分别是新建,可运行,终结,阻塞,等待,有时限等待

前面三个新建,运行,终结都是单向不可逆的,而后面的阻塞,等待,有时限等待是可以与运行状态来回变换的

在这里插入图片描述


线程池的核心参数

线程池的核心参数一共有7个

  • 核心线程数,是指一直保留在线程池里的线程数目
  • 最大线程数,是指核心线程数和救急线程数的总和
  • 生存时间,是指救急线程的存活时间,当救急线程空闲时间达到这个生存时间后,就会终结
  • 时间单位,是指存活时间的单位,是秒还是分钟或者小时
  • 阻塞队列,当核心线程用完后,剩下任务就在阻塞队列中排队,这里是指阻塞队列的长度
  • 线程工厂,给线程池创建新线程
    在这里插入图片描述

最后一个是拒绝策略,拒绝策略有四种,如下

在这里插入图片描述

当任务数量超过核心线程数量,最大排队数量以及临时线程数量的总和,全负载时,多余任务会根据线程池的拒绝策略来丢弃或处理


sleep和wait的区别

一个共同点,三个不同点

共同点

wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同

    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
      (这里简单可以理解,wait释放锁睡觉,sleep抱着锁睡觉)

在这里插入图片描述


lock 与 synchronized 的异同

可以从三个层面分解

不同点

①语法层面

  • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
  • Lock 是接口,源码由 jdk 提供,用 java 语言实现
  • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

②功能层面

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入(可以加多道锁)功能
  • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态(哪些线程被阻塞了)、公平锁、可打断、可超时(可设置最大超时时间,超过指定时间放弃获取锁)、多条件变量
  • Lock 有适合不同场景的实现,如 ReentrantLock(可重入锁), ReentrantReadWriteLock(读多写少场景)

③性能层面

  • 在没有竞争时(或竞争很少时),synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
  • 在竞争激烈时,Lock 的实现通常会提供更好的性能

Lock锁,是用创建的锁对象调用lock(),unlock()方法,上锁和释放锁
synchronized ,是同步代码块或同步方方法的方式


公平锁

  • 公平锁的公平体现
    • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
    • 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
    • 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
  • 公平锁会降低吞吐量,一般不用

条件变量

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

volatile能否保证线程安全

线程安全要考虑三个方面:可见性、有序性、原子性

①可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果

②有序性指,一个线程内代码按编写顺序执行

③原子性指,一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队


原子性

  • 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
  • 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性

可见性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

下面就是一个典型的永远循环-可见性案例

在这里插入图片描述
那到底是什么原因导致了线程0修改stop值后,主线程仍然继续循环,而线程1获取的stop又显示为true呢?

其中就是JIT的起的作用,JIT是优化器,对热点的字节码文件进行优化,例如频繁调用的方法,反复执行的循环;

在线程0还没有改变stop的值时,主线程在线程0睡眠的100毫秒内,已经执行了几百万次了,这时优化器JIT坐不住了,就对这个反复读取的stop且每次读取值都相同的字节码做优化,避免它频繁从内存中读取,把stop认为是false,把它的字节码编译成机器码缓存起来,再次循环时,就直接拿这个机器码,节省中间解释过程。如下图

在这里插入图片描述


若上面那个stop变量用了volatile修饰,JIT就不会对其优化,放过它
有序性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致
  • 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
  • 注意:
    • volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
    • volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
    • volatile 读写加入的屏障只能防止同一线程内的指令重排

写是上面操作不能越过下面(避免被程序之前的数据覆盖),读是下面操作不能越过上面(避免读取的是程序后面的数据)

是不能逆着箭头排的
在这里插入图片描述


悲观锁和乐观锁的区别

对比悲观锁与乐观锁

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
    • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
  • 乐观锁的代表是 AtomicInteger,使用 cas (compareAndSetInt方法,比较并交换的缩写)来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功
    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数

cas方法是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,把传入的旧值和当前共享变量的最新值作比较,来判断别人有没有修改过这个数据。

如果别人修改过,那么我再次获取现在最新的值。

如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

CAS是要和volatile一起使用,这样才能保证每次看到的共享变量是最新值


synchronized 加锁是使用的互斥方式,将多行代码作为一个整体执行,保证并发下的原子性

而CAS没有互斥,是并发执行,只是在修改操作时,用比较并交换的原则,来判断传入的旧值和当前获取的最新值是否一致(看看你要更新的值有没有被人修改过),一致才修改成功,不一致则修改失败,修改失败也无所谓,因为乐观锁是不断重试直至成功,在while(true)循环中,再拿到新值,在最新值得基础上做更新操作

下面是代码演示

synchronized 悲观锁

在这里插入图片描述


CAS 乐观锁

在这里插入图片描述


Hashtable 与 ConcurrentHashMap 的区别

Hashtable 对比 ConcurrentHashMap

  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

(Hashtable的底层实现是数组+链表结构实现的;hashtable的容量是质数,有较好的分布性,不需要进行二次哈希)


ConcurrentHashMap1.7和1.8的区别

ConcurrentHashMap 1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
  • 索引计算
    • 假设大数组长度是 2 m 2^m 2m,key 在大数组内的索引是 key 的二次 hash 值的高 m 位(例如:capacity为32,32是2的5次方,取二次hash值高5位,就是前5位,转十进制就得到存储位置索引值)
    • 假设小数组长度是 2 n 2^n 2n,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍;由于hashtable是加锁了的,即使链表使用头插法,也不会像ConcurrentHashMap那样导致死链
  • Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准,下图作为演示
    在这里插入图片描述
    新的小数组会根据当前segment[0]做为原型来创建
  • 小数组容量最小是2;小数组容量 = 大数组容量 / 可并发数

下面来个图,有图有真相
并发度决定蓝色数组的大小,容量决定小数组entry的大小
在这里插入图片描述


ConcurrentHashMap 1.8

  • 数据结构:Node 数组 + 链表或红黑树数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
    在这里插入图片描述

  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容

  • 扩容条件:Node 数组满 3/4 时就会扩容,1.7是超出才扩容

  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode

  • 扩容时并发 get

    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
      在这里插入图片描述

    • 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变

    • 如果链表最后几个元素扩容后索引不变,则节点无需复制
      在这里插入图片描述

  • 扩容时并发 put

    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
    • 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
    • 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
  • capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 2 n 2^n 2n

例如,要放16个元素(capacity为16),但是数组满扩容因子就会扩容,如果数组长度为16是不够的,所以数组容量 = capacity / factory;简单说就是capacity是数组容量的3/4.

  • loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
  • 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容

区别

①从底层结构上
1.7ConcurrentHashMap 底层数据结构是Segment(大数组) + HashEntry(小数组) + 链表
而1.8 ConcurrentHashMap 底层数据结构是 数组 + 链表或红黑树,而且链表添加元素上不同于1.7的头插法,而是尾插法

②从初始化的时机上
与 1.7 相比是懒惰初始化,1.7是饿汉式初始化,初始化以后,数组及数组零号元素已经创建出来了,1.8是在第一次put元素时,才会创建底层数组结构,是懒汉式初始化

③从扩容时机上
1.7是容量超出扩容因子才扩容
而1.8是数组 3/4(或扩容因子*数组容量) 时就会扩容


ThreadLocal的理解

  • ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
  • ThreadLocal 同时实现了线程间的线程隔离和线程内(只要是同一个线程,可以在多个方法中获取同一变量的)资源共享

原理

每个线程内有一个 ThreadLocalMap 类型的集合,用来存储资源对象

  • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
  • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
  • 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

线程间的资源是隔离的,key(ThreadLocal)可以相同,但关联的资源池可能不同

在这里插入图片描述

ThreadLocalMap
key 的 hash 值统一分配;初始容量为16;元素个数满2/3(扩容因子),数组扩容原来两倍,索引值相同时,不再使用链表那种拉链法,而是使用开放寻址法(从这索引开始,找下一个空闲位置作为索引位置)。


ThreadLocalMap中的key为何要设置为弱引用

弱引用 key

ThreadLocalMap 中的 key 被设计为弱引用,原因如下

  • Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存

内存释放时机

①被动 GC 释放 key

  • 仅是让 key 的内存释放,关联 value 的内存并不会释放

② 懒惰被动释放 value

  • get key 时,发现是 null key,则释放其 value 内存;(当ThreadLocalMap根据key去get值时,发现key不存在,会放入一个为null的key;这是和其他map的不同之处)
  • set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关

③主动 remove 释放 key,value

  • 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
  • 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收
    在这里插入图片描述
    所以①和②两种方法都不可用,最好还是主动回收掉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355207.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java】线程的死锁和释放锁

线程死锁是线程同步的时候可能出现的一种问题 文章目录1. 线程的死锁1.1 基本介绍1.2 应用案例2. 释放锁2.1 下面的操作会释放锁2.2 下面的操作不会释放锁1. 线程的死锁 1.1 基本介绍 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,…

第46章 自定义静态与数据库动态授权依赖注入的定义实现

1 数据库动态授权表授权原理 2 准备工作 2.1 重构Program.cs using Framework.Infrastructure.Extensions; var builder WebApplication.CreateBuilder(args); //如果启动项中不存在“appsettings.json”文件,则通过.Net(Core)的内置方法自动新建“appsettings.…

作为初学者必须要了解的几种常用数据库!

现在已经存在了很多优秀的商业数据库,如甲骨文(Oracle)公司的 Oracle 数据库、IBM 公司的 DB2 数据库、微软公司的 SQL Server 数据库和 Access 数据库。同时,还有很多优秀的开源数据库,如 MySQL 数据库,Po…

Django框架之模型视图-使用 PostMan 对请求进行测试

使用 PostMan 对请求进行测试 PostMan 是一款功能强大的网页调试与发送网页 HTTP 请求的 Chrome 插件,可以直接去对我们写出来的路由和视图函数进行调试,作为后端程序员是必须要知道的一个工具。 安装方式1:去 Chrome 商店直接搜索 PostMan…

链表OJ(四)链表排序合集

目录 合并两个排序的链表 合并k个已排序的链表 单链表的排序 链表的奇偶重排 链表的奇偶重排扩展 合并两个排序的链表 描述 输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。 数据范围: 0≤n≤…

Spark12: SparkSQL入门

一、SparkSQL Spark SQL和我们之前讲Hive的时候说的hive on spark是不一样的。hive on spark是表示把底层的mapreduce引擎替换为spark引擎。而Spark SQL是Spark自己实现的一套SQL处理引擎。Spark SQL是Spark中的一个模块,主要用于进行结构化数据的处理。它提供的最核…

Kubernetes入门级教程

Kubernetes入门级教程1. Introduction1.1 概述1.2 关键字介绍2. Cluster Install2.1 Big Data -- Postgres3. 基础知识3.1 Pod3.2 控制器3.3 通讯模式3.4 服务发现4. Command4.0 编辑文件4.1 在宿主机执行命令4.2 创建资源对象4.3 查询资源对象4.4 查询资源描述4.5 修改资源4.6…

Linux 交换分区与链接文件

目录 SWAP交换分区扩展 fdisk 创建分区 mkswap 将逻辑分区/主分区格式化为交换分区(make swap) swapon 交换分区挂载 swapoff 卸载交换分区 vim /etc/fstab 永久挂载 将文件设置为交换分区 链接文件 软链接 硬链接 SWAP交换分区扩展 交换分区…

量子力学奇妙之旅-双态系统(后)

专栏: 高质量文章导航-持续更新中 引子: 感慨:对于还原论,物质深层结构的物理定律如此的复杂,求解一个简单的双态系统已经如此困难,运用了大量的近视方法,在宇宙真理面前,我们只是虫子啊,我们固有的概念里面对逻辑自洽性,对事物发展的可预测性必然性,真实世界的有…

2023美赛F题讲解+数据领取

我们给大家准备了F题的数据,免费领取!在文末 国内生产总值(GDP)可以说是一个国家经济健康状况最著名和最常用的指标之--。它通常用于确定一个国家的购买力和获得贷款的机会,为各国提出提高GDP的政策和项目提供动力。GDP“衡量一个国家在给定时间段内生产…

docker中 gitlab 安装、配置和初始化

小笔记:gitlab配置文件 /etc/gitlab/gitlab.rb 配置项jcLee95 的CSDN博客:https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 邮箱 :291148484163.com 本文地址:https://blog.csdn.net/qq_28550263/article/details/1…

运动款蓝牙耳机哪个品牌好、市面最火爆的运动耳机推荐

我们都知道运动最不可或缺的就是音乐了,它俩是天生的好搭档,所以凡是很经常运动的小伙伴一定会去单独选择一款超好用的运动耳机,来增强运动体验效果,那么市面上的运动耳机那么多,怎么选择一款好用的运动耳机呢&#xf…

MySql 函数

1、简述 函数 是指一段可以直接被另一段程序调用的程序或代码。 也就意味着,这一段程序或代码在MySQL中已经给我们提供了,我们要做的就是在合适的业务场景调用对应的函数完成对应的业务需求即可。 MySQL中的函数主要分为以下四类: 字符串函数…

【TypeScrip】TypeScrip的任意类型(Any 类型 和 unknown 顶级类型):

文章目录一、安转依赖:【1】nodejs 环境执行ts【2】使用ts-node二、Any 类型 和 unknown 顶级类型【1】没有强制限定哪种类型,随时切换类型都可以 我们可以对 any 进行任何操作,不需要检查类型【2】声明变量的时候没有指定任意类型默认为any【…

基于SSM框架的生活论坛系统的设计与实现

基于SSM框架的生活论坛系统的设计与实现 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景…

已解决ImportError: cannot import name ‘featureextractor‘ from ‘radiomics‘

已解决from radiomics import featureextractor导包,抛出ImportError: cannot import name ‘featureextractor‘ from ‘radiomics‘异常的正确解决方法,亲测有效!!! 文章目录报错问题报错翻译报错原因解决方法联系博…

centos7给已有分区进行扩容

1、背景 最近我在虚拟机上安装软件,发现磁盘空间不足,通过上网查找资料,发现可以通过如下方法进行磁盘扩容,此处进行记录一下。 2、实现扩容 1、虚拟机上添加一个新的硬盘 2、查看我们刚刚加入的硬盘 此处我们可以看到/dev/nvm…

Seata架构篇 - TCC模式

TCC 模式 概述 TCC 是分布式事务中的两阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel)。Try:对业务资源的检查并…

【MySQL进阶】视图 存储过程 触发器

😊😊作者简介😊😊 : 大家好,我是南瓜籽,一个在校大二学生,我将会持续分享Java相关知识。 🎉🎉个人主页🎉🎉 : 南瓜籽的主页…

Unity3D -知识点(1)

1.场景视图鼠标滚轮:场景放大缩小鼠标右键:场景左右平移场景编辑器中,能看到什么?网格,每一格大小为1unit,建模不同,规定不同,(对应屏幕上100个像素)世界坐标系y轴向上为正x轴向右为…