当前位置: 首页 > news >正文

【多线程】进阶

目录

一、CAS

什么是 CAS

CAS 伪代码

CAS 有哪些应用

实现原子类

CAS 是怎么实现的

如何通过 CAS 实现的原子类

CAS 的 ABA 问题

什么是 ABA 问题

ABA 问题引来的 BUG

正常的过程 

异常的过程

解决方案

📝相关面试题

二、JUC(java.util.concurrent)的常见类

Callable 接口

理解 Callable

理解 FutureTask

ReentrantLock

ReentrantLock 和 synchronized 的区别:

原子类

线程池

信号量 Semaphore

理解信号量

CountDownLatch

📝相关面试题

三、线程安全的集合类

多线程环境使用 ArrayList

多线程环境使用队列

多线程环境使用哈希表

📝相关面试题


一、CAS

什么是 CAS

全称 Compare and swap ,字面意思就是 “比较并交换”,一个 CAS 涉及到以下操作:

我们假设内存中的原数据 V,旧的预期值 A,需要修改的新值 B

1️⃣比较 A 与 V 是否相等。(比较)

2️⃣如果比较相等,将 B 写入 V。(交换)

3️⃣返回操作是否成功。

〰️比较内存和 CPU 寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)。一个内存的数据和两个寄存器中的数据进行操作(寄存器 1 和寄存器 2),比较内存和寄存器 1 中的值是否相等,如果不相等,就无事发生,如果相等,就交换内存和寄存器 2 的值(此处一般只是关心,内存交换后的内容,不关心寄存器 2 交换后的内容),虽然叫做 "交换" 实际上,希望达成的效果是 “赋值”。

CAS 伪代码

下面写的代码不是原子的,真实的 CAS 是⼀个原子的硬件指令完成的。 这个伪代码只是辅助理解 CAS 的工作流程,且下面的伪代码其实是有一条 CPU 指令完成的。

boolean CAS(address, expectValue, swapValue) {

               if (&address == expectedValue) {

                   &address = swapValue;

               return true;

       } return false;

CAS 的关键不在于这个逻辑是干啥,而是在于,通过 "一个CPU指令"(原子的)完成了上述一系列的操作,因此 CAS 就能给编写多线程代码,带来新的思路〰️“无锁化编程

CAS 有哪些应用

实现原子类

int/long 在进行➕➕、➖➖的时候(会经历 load、add、save 三步)都不是原子的,然而基于 CAS 实现的原子类,对 int/Iong 等这些类型进行了封装,从而可以原子的完成 ➕➕、➖➖ 等操作。

原子类,在Java标准库中,也有现成的实现。标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 Atomiclnteger 类,其中的 getAndIncrement 相当i➕➕ 操作。

👁‍🗨➕➕操作的线程不安全代码

public class Demo36 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

👁‍🗨使用 Atomiclnteger 类实现线程安全并且不用加锁的代码

import java.util.concurrent.atomic.AtomicInteger;public class Demo36 {// private static int count = 0;private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();   // count++;
//                count.incrementAndGet();   //++count;
//                count.getAndIncrement();   //count--;
//                count.decrementAndGet();   //--count;
//                count.getAndAdd(10); //count+= 10;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// count++;count.getAndIncrement();   // count++;}});t1.start();t2.start();t1.join();t2.join();//通过 count.get() 拿到原子类内部持有的真实数据System.out.println("count = " + count);}
}

其它类在 Java 文档中都有,附下图

CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
🔹java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作;
🔹unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
🔹Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

简而言之,是因为硬件予以了支持,软件层面才能做到

如何通过 CAS 实现的原子类

分析给出的伪代码

📷图解

假设两个线程同时调用 getAndIncrement 

1️⃣两个线程都读取 value 的值到 oldValue 中.(oldValue 是一个局部变量,在栈上.每个线程有自己的栈)

2️⃣线程 2 先执行 CAS 操作.由于 oldValue 和 value 的值相同,直接进行对 value 赋值
注意:
CAS 是直接读写内存的,而不是操作寄存器.
CAS 的读内存,比较,写内存操作是一条硬件指令,是原子的.

3️⃣线程 1 再执行 CAS 操作,第一次 CAS 的时候发现 oldValue 和 value 不相等不能进行赋值.因此需要进入循环.
在循环里重新读取 value 的值赋给 oldValue

4️⃣线程 1 接下来第二次执行 CAS,此时 oldValue 和 value 相同,于是进行赋值操作.

5️⃣线程 1 和线程 2 返回各自的 oldValue 的值即可.

通过形如上述代码就可以实现一个原子类.不需要使用重量级锁,就可以高效的完成多线程的自增操作.
本来 check and set 这样的操作在代码角度不是原子的.但是在硬件层面上可以让一条指令完成这个
操作,也就变成原子的了. 

上面的这个过程,类似于,网购一个东西,确认收货的过程。比如买一个笔记本电脑/手机,有的人,直接就拆了开始使用了(有可能买到了二手机/翻新机),也有的人,会先做详细的检查,检查商品的序列号,去官网上对比。如果发现机器没有问题,才真正进行后续的使用(没有识别到别人使用的痕迹),如果发现机器有问题,有别人使用的痕迹,就得及时进行售后。

CAS 的 ABA 问题

什么是 ABA 问题

假设存在两个线程 t1 和 t2.有一个共享变量 num,初始值为 A.
接下来,线程 t1 想使用 CAS 把 num 值改成 Z,那么就需要
🔹先读取 num 的值,记录到 oldNum 变量中.
🔹使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z.
但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A

线程 t1 的 CAS 是期望 num 不变就修改.但是 num 的值已经被 t2 给改了.只不过又改成 A了.这个时候 t1 究竟是否要更新 num 的值为 Z 呢?

到这一步,t1 线程无法区分当前这个变量始终是 A,还是经历了一个变化过程.

这就好比,我们买一个手机,无法判定这个手机是刚出厂的新手机,还是别人用引旧了,又翻新过的手机

ABA 问题引来的 BUG

大部分的情况下,t2 线程这样的一个反复横跳改动,对于 t1 是否修改 num 是没有影响的.但是不排除一些特殊情况.

假设滑稽老哥有 1000 存款.滑稽想从 ATM 取 500 块钱.取款机创建了两个线程,并发的来执行 -500 操作.
我们期望一个线程执行 -500 成功,另一个线程 -500 失败.
如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.

正常的过程 

存款 1000.线程 1 获取到当前存款值为 1000,期望更新为 500;线程 2 获取到当前存款值为1000,期望更新为 500.
线程 1 执行扣款成功,存款被改成 500.线程 2 阻塞等待中
轮到线程 2 执行了,发现当前存款为 500,和之前读到的 1000 不相同,执行失败.

异常的过程

存款 1000.线程 1 获取到当前存款值为 1000,期望更新为 500;线程 2 获取到当前存款值为 1000,期望更新为 500.
线程 1 执行扣款成功,存款被改成 500.线程 2 阻塞等待中,
在线程 2 执行之前,滑稽的朋友正好给滑稽转账 500,账户余额变成 1000!!
轮到线程 2 执行了,发现当前存款为 1000,和之前读到的 1000 相同,再次执行扣款操作
这个时候,扣款操作被执行了两次!!!都是 ABA 问题搞的鬼!!

解决方案

给要修改的值,引入版本号.在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期.


●CAS 操作在读取旧值的同时,也要读取版本号.
●真正修改的时候,
  。如果当前版本号和读到的版本号相同,则修改数据,并把版本号 +1.
  。如果当前版本号高于读到的版本号.就操作失败(认为数据已经被修改过了).

这就好比,判定这个手机是否是翻新机,那么就需要收集每个手机的数据,第一次挂在电商网站上的手机记为版本 1,以后每次这个手机出现在电商网站上,就把版本号进行递增.这样如果买家不在意这是翻新机,就买.如果买家在意,就可以直接略过.

⬆️对比理解上面的转账例子

假设滑稽老哥有 1000 存款.滑稽想从 ATM 取 500 块钱.取款机创建了两个线程,并发的来执行 -500 操作.
我们期望一个线程执行 -500 成功,另一个线程 -500失败.
为了解决 ABA 问题,给余额搭配一个版本号,初始设为 1.

存款 1000.线程 1 获取到存款值为 1000,版本号为 1,期望更新为 500;线程 2 获取到存款值为1000,版本号为 1,期望更新为500.
线程 1 执行扣款成功,存款被改成 500,版本号改为 2.线程 2 阻塞等待中.
在线程 2 执行之前,滑稽的朋友正好给滑稽转账 500,账户余额变成1000,版本号变成 3.
轮到线程 2 执行了,发现当前存款为 1000,和之前读到的 1000 相同,但是当前版本号为 3,之前读到的版本号为 1,版本小于当前版本,认为操作失败.

实际开发中,一般很少直接使用 CAS,都是大佬封装好了,直接进行使用现成的操作,因为 CAS本身使用起来有点复杂的.

在 Java 标准库中提供了 AtomicStampedReference<E> 类.这个类可以对某个类进行包装,在内
部就提供了上面描述的版本管理功能

关于 AtomicStampedReference<E> 的具体用法此处不再展开,有需要的同学自行查找文档了解使用方法即可.

📝相关面试题

1️⃣讲解下你自己理解的 CAS 机制

全称Compare and swap,即 "比较并交换" 相当于通过一个原子的操作,同时完成 "读取内存,比较是否相等,修改内存" 这三个步骤.本质上需要 CPU 指令的支撑.

2️⃣ABA  问题怎么解决

给要修改的数据引入版本号.在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增;如果发现当前版本号比之前读到的版本号大,就认为操作失败.

二、JUC(java.util.concurrent)的常见类

Callable 接口

Callable 是一个 interface.相当于把线程封装了一个 "返回值".方便程序猿借助多线程的方式计算结
果.

👁‍🗨创建线程计算1+2+3++1000,不使用 Callable 版本

class Demo37 {private static int result;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {int sum = 0;for (int i = 0; i < 1000; i++) {sum += i;}result = sum;});t.start();t.join();System.out.println("result =" + result);}
}

👁‍🗨创建线程计算1+2+3++1000,使用Callable版本

▪️创建一个匿名内部类,实现 Callable 接口.Callable 带有泛型参数.泛型参数表示返回值的类型.
▪️重写 Callable 的 call 方法,完成累加的过程.直接通过返回值返回计算结果.
▪️把 callable 实例使用 FutureTask 包装一下.
▪️创建线程,线程的构造方法传入 FutureTask.此时新线程就会执行 FutureTask 内部的 Callable的 call 方法,完成计算.计算结果就放到了 FutureTask 对象中.
▪️在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕.并获取到 FutureTask 中的结果.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo37 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}return sum;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();System.out.println(futureTask.get());}
}

理解 Callable

类似于 Runnable ,可以表示有一个具体的任务,通过 run 方法来描述任务的内容

🏸Callable 和 Runnable 相对,都是描述一个 "任务".Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.
🏸Callable 通常需要搭配 FutureTask 来使用.FutureTask 用来保存 Callable 的返回结果.因为
Callable 往往是在另一个线程中执行的,啥时候执行完并不确定.
🏸FutureTask 就可以负责这个等待结果出来的工作.

理解 FutureTask

想象去吃麻辣烫.当餐点好后,后厨就开始做了.同时前台会给你一张"小票".这个小票就是
FutureTask.后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.

ReentrantLock

可重入互斥锁.和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全

ReentrantLock 也是可重入锁."Reentrant" 这个单词的原意就是 "可重入"

ReentrantLock 的用法:经典风格的锁,通过 lock 和 unlock 方法来完成加锁解锁
▫️lock():加锁,如果获取不到锁就死等.
▫️trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁.
▫️unlock():解锁

import java.util.concurrent.locks.ReentrantLock;public class Demo38 {private static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {lock.lock();count++;lock.unlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {lock.lock();count++;lock.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

实际开发中,大多数情况下,使用 synchronized 即可.
ReentrantLock 相比 synchronized 还是有一些区别差异的.

ReentrantLock 和 synchronized 的区别:

🌀synchronized 是一个关键字,是 JVM 内部实现的(基于 C++ 实现).ReentrantLock 是标准库的一个类,在 JVM 外实现的(基于 Java 代码实现).

🌀synchronized 使用时不需要手动释放锁.ReentrantLock 使用时需要手动释放.使用起来更灵活,但是也容易遗漏 unlock.可以把 unlock 放到 finally 中.

💠synchronized 在申请锁失败时,会死等.ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.在加锁失败的时候不会阻塞,而是直接返回,通过返回值来反馈是加锁成功还是失败.

💠synchronized 是非公平锁,ReentrantLock 默认是非公平锁.可以通过构造方法传入一个 true 开启公平锁模式.

💠更强大的唤醒机制.synchronized 是通过 Object 的 wait/notify 实现等待-唤醒.每次唤醒的是一个随机等待的线程.ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

锁竞争不激烈的时候,使用 synchronized,效率更高,自动释放更方便.
锁竞争激烈的时候,使用 ReentrantLock,搭配 trylock 更灵活控制加锁的行为,而不是死等.
如果需要使用公平锁,使用 ReentrantLock.

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 ➕➕ 高很多。原子类有以下几个

AtomicBoolean
Atomiclnteger
AtomiclntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
以 Atomiclnteger 举例,常见方法有

addAndGet(int delta);     i += delta;

decrementAndGet();                  --i;

getAndDecrement();                  i--;
incrementAndGet();                 ++i;
getAndIncrement();                  i++;

详见文章第一部分内容

线程池

详细内容请跳转到此篇文章进行查看

信号量 Semaphore

信号量,用来表示 "可用资源的个数". 本质上就是⼀个计数器.

理解信号量

可以把信号量想象成是停车场的展示牌:当前有车位 100 个.表示有 100 个可用资源
当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1(这个称为信号量的 P 操作)
当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1(这个称为信号量的 V 操作)
如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源

Semaphore 的 PV 操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用.

👁‍🗨代码示例

import java.util.concurrent.Semaphore;public class Demo39 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(3);semaphore.acquire();System.out.println("申请一个资源");semaphore.acquire();System.out.println("申请一个资源");semaphore.acquire();System.out.println("申请一个资源");semaphore.release();System.out.println("释放一个资源");semaphore.acquire();System.out.println("申请一个资源");}
}

用信号量还可以实现加锁解锁效果🔒🔓

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;public class Demo38 {private static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {
//                lock.lock();try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}
//                lock.unlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {
//                lock.lock();try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}
//                lock.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

CountDownLatch

同时等待 N 个任务执行结束.

好像跑步比赛,10 个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

很多时候,需要把一个大的任务,拆成多个小任务,通过 多线程/线程池 执行.
那么如何衡量,所有的任务都执行完毕了?

🔸构造 CountDownLatch 实例,初始化 20 表示有 20 个任务需要完成.

🔸每个任务执行完毕,都调用 countDownLatch.countDown().在 CountDownLatch 内部的计数器同时自减.

🔸主线程中使用 countDownLatch.await(); 阻塞等待所有任务执行完毕.相当于计数器为 0 了.

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo40 {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(4);//构造方法的数字,就是拆分出来的任务个数CountDownLatch countDownLatch = new CountDownLatch(20);for (int i = 0; i < 20; i++) {int id = i;executorService.submit(() -> {System.out.println("下载任务 " + id + "");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("下载任务 " + id + " 结束执行");//完毕 overcountDownLatch.countDown();});}// 当 countDownLatch 收到了 20 个 “完成”,所有的任务就都完成了// await => all wait// await 这个词也是计算机术语,在python / js 意思是 async wait(异步等待)countDownLatch.await();System.out.println("所有任务都完成");}
}

📝相关面试题

1️⃣线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

2️⃣为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,
●synchronized 使用时不需要手动释放锁.ReentrantLock 使用时需要手动释放.使用起来更灵活.
●synchronized 在申请锁失败时,会死等.ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
●synchronized 是非公平锁,ReentrantLock 默认是非公平锁.可以通过构造方法传入一个 true 开启公平锁模式.
●synchronized 是通过 Object 的 wait/notify 实现等待-唤醒.每次唤醒的是一个随机等待的线程.
ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程

3️⃣AtomicInteger 的实现原理是什么?

基于 CAS 机制. 伪代码如下:

执行过程参考上面 CAS "有哪些应用" 部分. 

4️⃣信号量听说过么?之前都用在过哪些场景下?

信号量,用来表示 "可用资源的个数".本质上就是一个计数器
使用信号量可以实现 "共享锁",比如某个资源允许 3 个线程同时使用,那么就可以使用 P 操作作为加锁,V 操作作为解锁,前三个线程的 P 操作都能顺利返回,后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.

5️⃣解释⼀下 ThreadPoolExecutor 构造方法的参数的含义

参考 ThreadPoolExecutor 章节https://blog.csdn.net/2301_80141037/article/details/146104441?spm=1001.2014.3001.5502

三、线程安全的集合类

原来的集合类,大部分都不是线程安全的
Vector,Stack,HashTable 是线程安全的(内置了 synchronized 无脑加锁,所以不建议用),其他的集合类不是线程安全的

多线程环境使用 ArrayList

1️⃣自己使用同步机制 (synchronized 或者 ReentrantLock),自己加锁

前面做过很多相关的讨论了.此处不再展开.

2️⃣Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的⼀个基于 synchronized 进行线程同步的 List. synchronizedList 的关键操作上都带有 synchronized

如果需要使用 ArrayList/LinkedList 这样的结构,在标准库中提供了一个带锁的 List

3️⃣使用 CopyOnWriteArrayList

CopyOnWrite 容器即写时复制的容器。没有加锁,通过写时复制来实现线程安全。

🥛当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Coy,复制出一个新的容器,然后新的容器里添加元素,
🥛添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下,性能很高,不需要加锁竞争.因此,当前写时拷贝机制主要就是用来应对多个线程读,一个线程写这样的场景.
缺点:
1.占用内存较多.
2.新写的数据不能被第一时间读取到.

并且如果是针对多个线程同时写的情况,写实拷贝机制就难以应对

多线程环境使用队列

1.ArrayBlockingQueue

基于数组实现的阻塞队列

2.LinkedBlockingQueue

基于链表实现的阻塞队列

3.PriorityBlockingQueue

基于实现的带优先级的阻塞队列

4.TransferQueue

最多只包含一个元素的阻塞队列

多线程环境使用哈希表

HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用:
●Hashtable(不推荐)
●ConcurrentHashMap(Java方向top5 超高频面试题)🎯🎯🎯

此处的这个数据结构,相比于 HashMap 和 Hashtable 来说,改进力度是非常大的

1️⃣优化了锁的粒度[最核心]

Hashtable只是简单的把关键方法加上了 synchronized 关键字  

这相当于直接针对 Hashtable 对象本身加锁.使整个哈希表对象就是一把锁,任何一个针对这个哈希表的操作都会触发锁竞争

🔹如果多线程访问同一个 Hashtable 就会直接造成锁冲突.

ConcurrentHashMap 是给每个 hash 表中的 "链表" 进行加锁.(不是一把锁,而是多把锁)-"锁桶"

🔸写操作加锁的方式仍然是用 synchronized,但是不是锁整个对象,而是 "锁桶"(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率.

 2️⃣ConcurrentHashMap 引入了 CAS 原子操作,针对像修改 size 这样的操作,直接借助 CAS 完成,并不会加锁.

🔹size 属性(元素的个数)也是通过 synchronized 来控制同步,也是比较慢的.

🔸充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

3️⃣针对读操作,做了特殊处理.

🔸读操作没有加锁(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁.确保读操作不会读到 "修改一半" 的数据.

4️⃣针对 hash 表的扩容,进行了特殊的优化.

🔹普通 hash 表一旦触发扩容,需要创建新的 hash 表,把元素都搬运过去,就由该线程完成整个扩容过程.在一次 put 就完成了,这个过程会涉及到大量的元素拷贝,效率会非常低.

🔸优化了扩容方式: 化整为零

    ▪️发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去.

    ▪️扩容期间,新老数组同时存在.

    ▪️后续每个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程.每个操作负责搬运一小部分元素.

    ▪️搬完最后一个元素再把老数组删掉.

    ▪️这个期间,插入只往新数组加.

    ▪️这个期间,查找需要同时查新数组和老数组.

📝相关面试题

1️⃣ConcurrentHashMap 的读是否要加锁,为什么?

读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据,搭配了 volatile 关键字.

2️⃣介绍下 ConcurrentHashMap 的锁分段技术?

这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个 "段" (Segment),针对每个段分别加锁.

目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争.

3️⃣ConcurrentHashMap 在 jdk1.8 做了哪些优化?

取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).

将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8 个元素)就转换成红黑树.

4️⃣Hashtable 和 HashMap、ConcurrentHashMap 之间的区别?

HashMap: 线程不安全. key 允许为 null.

Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象,效率较低. key 不允许为 null.

ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点,锁冲突概率低,充分利用 CAS 机制. 优化了扩容方式. key 不允许为 null.

相关文章:

【多线程】进阶

目录 一、CAS 什么是 CAS CAS 伪代码 CAS 有哪些应用 实现原子类 CAS 是怎么实现的 如何通过 CAS 实现的原子类 CAS 的 ABA 问题 什么是 ABA 问题 ABA 问题引来的 BUG 正常的过程 异常的过程 解决方案 &#x1f4dd;相关面试题 二、JUC(java.util.concurrent)的…...

MFC中字符串string类型和CString类型互转方法

文章目录 一、CString 转 std::string1. Unicode项目&#xff08;_UNICODE 已定义&#xff09;2. 多字节项目&#xff08;_MBCS 已定义&#xff09; 二、std::string 转 CString1. Unicode项目&#xff08;_UNICODE 已定义&#xff09;2. 多字节项目&#xff08;_MBCS 已定义&a…...

工作记录 2017-03-13

工作记录 2017-03-13 序号 工作 相关人员 1 修改邮件上的问题。 开始处理操作日志部分。 测试了C#和MySql的连接。 更新RD服务器。 郝 更新的问题 1、 修改了CMS1500的打印&#xff0c;NDC的内容用了小的字体。 2、在Cliams List中可以查看Job的Notes。 3、Payment Po…...

Linux 配置NFS服务器

1. 开放/nfs/shared目录&#xff0c;供所有用户查阅资料 服务端 &#xff08;1&#xff09;安装nfs服务&#xff0c;nfs-utils包中包含rpcbind&#xff08;rpc守护进程&#xff09; [rootnode1-server ~]# yum install -y nfs-utils # nfs-utils包中包含rpcbind [rootnode…...

Linux《进程概念(上)》

在之前的Linux学习当中我们已经了解了基本的Linux指令以及基础的开发工具的使用&#xff0c;那么接下来我们就要开始Linux当中一个非常重要的部分的学习——进程&#xff0c;在此进程是我们之后Linux学习的基础&#xff0c;并且通过进程的学习会让我们了解更多的操作系统的相关…...

游戏开发中的贝塞尔曲线:感受丝滑的数学之美

这是一篇vip文章,如果你还不是vip,可以移步https://www.ilikexff.cn/articles/165免费阅读。 介绍 贝塞尔曲线是计算机图形学中最重要的概念之一,以其在表示曲线时的灵活性和精确性而闻名。广泛应用于计算机图形学、动画、路径规划等领域的数学曲线。 贝塞尔曲线的数学原理基…...

Java【多线程】(6)定时器

目录 1.前言 2.正文 2.1库中定时器 2.2手搓定时器 3.小结 1.前言 哈喽大家好呀&#xff0c;今天继续给大家分享Java中定时器的学习&#xff0c;正文包括定时器的三种实现方式&#xff0c;正文如下。 2.正文 在 Java 中&#xff0c;定时器&#xff08;Timer&#xff09;…...

Epub转PDF软件Calibre电子书管理软件

Epub转PDF软件&#xff1a;Calibre电子书管理软件 一款好用的电子书管理软件&#xff0c;可快速导入电脑里的电子书并进行管理&#xff0c;支持多种格式&#xff0c;阅读起来非常方便。同时也有电子书格式转换功能。 第一步&#xff1a;添加电子书 将需要转换的电子书添加到…...

使用FastExcel时的单个和批量插入的问题

在我们用excel表进行插入导出的时候&#xff0c;通常使用easyexcel或者FastExcel&#xff0c;而fastexcel是easy的升级版本&#xff0c;今天我们就对使用FastExcel时往数据库插入数据的业务场景做出一个详细的剖析 场景1 现在我们数据库有一张组织表&#xff0c;组织表的字段…...

nginx https配置

一.https配置 HTTPS 协议是由HTTP 加上TLS/SSL 协议构建的可进行加密传输、身份认证的网络协议&#xff0c;主要通过数字证书、加密算法、非对称密钥等技术完成互联网数据传输加密&#xff0c;实现互联网传输安全保护。 1.生成证书 openssl genrsa -des3 -out server.key 20…...

git --- cherry pick

git --- cherry pick cherry pick cherry pick Cherry Pick 是 Git 中的一个操作&#xff0c;它允许你选择某个分支的某次&#xff08;或多次&#xff09;提交&#xff0c;并将其应用到当前分支&#xff0c;而不会合并整个分支的所有更改。 cherry pick 的作用 只提取某个特定的…...

虚拟机安装linux系统无法上网的解决方法

在虚拟环境中运行Linux系统时&#xff0c;有时会遇到网络连接问题&#xff0c;特别是在使用虚拟机软件如VMware或VirtualBox时。本文将详细介绍一种针对“虚拟机安装Linux系统无法上网”问题的解决方案&#xff0c;以CentOS 6.5为例&#xff0c;适用于其他基于NAT模式的虚拟机环…...

北大人工智能研究院朱松纯:“中国的AI叙事” 存在认知偏差

3月29日&#xff0c;在2025中关村论坛通用人工智能论坛上&#xff0c;北京通用人工智能学院院长&#xff0c;北京大学人工智能研究院、智能学院院长朱松纯表示&#xff0c;目前&#xff0c;行业对AI的讨论几乎被大模型能力所占据&#xff0c;而基础学科、原始创新与智能本质的研…...

Java高频面试之集合-20

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本baby今天来报道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面试官&#xff1a;讲讲 HashSet 的底层实现&#xff1f; HashSet 是 Java 集合框架中用于存储唯一元素的高效数据结构&#xff0c;其底层实…...

使用Qemu模拟32位ARM系统

一、环境 实验环境如下&#xff1a; 主机&#xff1a;x86_64 操作系统&#xff1a;Ubuntu 20.04.6 LTS Qemu版本&#xff1a;QEMU emulator version 4.2.1 Linux内核版本&#xff1a;linux-4.4.240 Busybox版本&#xff1a;busybox-1.35.0二、前置准备 下载 linux-4.4.240 源…...

【初阶数据结构】栈

文章目录 一、概念与结构 二、栈的实现 栈的定义 1.初始化 2.入栈 3.判断栈是否为空 4.出栈 5.取栈顶元素 6.获取栈中有效元素个数 2.销毁 三、完整码源 总结 一、概念与结构 栈&#xff1a; 一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据…...

docker-compose部署prometheus+grafana+node_exporter

目录 docker-compose文件 配置文件 文件层级关系&#xff0c;docker-compose和配置文件位于同级目录 node_exporter页面json文件 涉及离线包 一.docker-compose文件 [rootsulibao prometheus]# cat docker-compose.yml version: 3services:prometheus:image: registry.c…...

maya调整全局关节显示大小

请按以下步骤操作&#xff1a; 在 Maya 主菜单栏中&#xff0c;找到 Display (显示) 菜单。 在 Display 菜单下&#xff0c;找到 Animation (动画) 子菜单。 在 Animation 子菜单中&#xff0c;点击 Joint Size... (关节大小...)。 这时会弹出一个小窗口或者直接在界面上出现…...

“屏幕“的实现_程序中如何将数据映射到硬件_C++实战

前言 程序里的数据,最后都需要将数据对象写入硬件.C/C最大的优势体现也是在这里,他既是高级语言方便被程序员使用,又能和硬件沟通. 引入 以"屏幕"的实现,总结数据映射到硬件的代码写法 分析 软件部分 1.屏幕是数据对象---一切都是数据,一切都是对象;数据有类型,屏…...

R --- Error in library(***) : there is no package called ‘***’ (服务器非root用户)

步骤 步骤一&#xff1a;在自己目录下创建R包安装路径步骤二&#xff1a;配置用户本地的R库路径步骤三&#xff1a;安装缺失的包&#xff08;在终端&#xff09;步骤四&#xff1a;验证安装 步骤一&#xff1a;在自己目录下创建R包安装路径 mkdir -p ~/R_libs步骤二&#xff1…...

Go中的逃逸分析

什么是逃逸&#xff1f; 逃逸是指一个变量本来应该分配在栈&#xff08;stack&#xff09;上&#xff0c;但由于某些原因&#xff0c;最终被分配到了堆&#xff08;heap&#xff09;上。 类比&#xff1a; 栈就像一个临时的快餐盒&#xff0c;用来存放短期使用的数据。堆就像…...

解决 Android AGP 最新版本中 BuildConfig 报错问题

在最新版本的 Android Gradle Plugin (AGP) 中&#xff0c;Google 对构建系统做了不少改动&#xff0c;可能会导致一些与 BuildConfig 相关的问题。以下是常见问题及解决方案&#xff1a; 常见问题及修复方法 1. BuildConfig 类完全缺失 原因&#xff1a;AGP 8.0 默认不再为库模…...

Rollup系列之安装和入门

Rollup ‌Rollup.js‌的主要用途是将小的代码片段编译成更大、更复杂的代码&#xff0c;例如库或应用程序。它特别适用于将ES模块编译成不同的模块形式&#xff0c;如AMD、CommonJS、UMD等&#xff0c;以便在不同的环境中使用‌。 Rollup的应用场景与好处&#xff1a; 插件或…...

Kafka 4.0 发布:KRaft 替代 Zookeeper、新一代重平衡协议、点对点消息模型、移除旧协议 API

KRaft 全面替代 ZooKeeper Apache Kafka 4.0 是一个重要的里程碑&#xff0c;标志着第一个完全无需 Apache ZooKeeper 运行的主要版本。 通过默认运行在 KRaft 模式下&#xff0c;Kafka 简化了部署和管理&#xff0c;消除了维护单独 ZooKeeper 集群的复杂性。 这一变化显著降…...

MQTT之重复消息(6、在项目中遇到的问题)

项目背景: 在 Spring Boot MQTT 5.0 环境中&#xff0c;RTU设备向SpringBoot平台发送心跳数据、业务监控数据。同时SpringBoot平台可以向RTU设备下发指令&#xff0c;RTU在执行完指令之后向平台发送响应数据。 问题一、SpingBoot平台发送指令给RTU设备&#xff0c;RTU设备能够…...

8、linux c 信号机制

一、信号概述 1. 信号概念 信号是一种在软件层次上对中断机制的模拟&#xff0c;是一种异步通信方式。信号的产生和处理都由操作系统内核完成&#xff0c;用于在进程之间传递信息或通知某些事件的发生。 2. 信号的产生 信号可以通过以下方式产生&#xff1a; 按键产生&…...

Set,Map,WakeSet,WakeMap

简介 Set、Map、WeakMap 和 WeakSet 是 ES6 引入的高级数据结构&#xff0c;它们的底层实现和特性与传统的对象和数组有显著差异 强弱引用了解: link Set ​Set对象 是一种用于存储 ​唯一值 的可迭代集合&#xff0c;可存储任意类型的值&#xff08;原始值、对象引用等&…...

NSSCTF(MISC)—[HITCTF 2021]PNG

相应的做题地址&#xff1a;https://www.nssctf.cn/problem/819 import zlib from Crypto.Cipher import AES import base64 def decode(data, key, iv): cipher AES.new(key, AES.MODE_CBC, iv) decryptByts base64.b64decode(data) msg cipher.decrypt(decryptByts) msgs…...

只出现一次的数字

这个题目动了点脑筋&#xff0c;由于它们时无序的&#xff0c;所以我们如果去找的话比较费劲&#xff0c;可能要循环嵌套再嵌套&#xff0c;所以我们先利用库中自带的sort函数进行排序&#xff0c;把这些数从小到大以此排列&#xff0c;然后我们进行判断哪个数出现了一次即可。…...

【编程中的框架】

编码中常用的框架及其使用方法和好处 框架&#xff08;Framework&#xff09;是一种为解决特定问题而设计的软件架构&#xff0c;它提供了一组预定义的组件、模式和工具&#xff0c;帮助开发者更高效地构建应用程序。框架通常不仅仅是方法库&#xff0c;它们提供了一种结构化的…...

Python-常用关键字

基础值 1. False - 意义&#xff1a;布尔类型假值&#xff08;首字母大写&#xff09; - 用法示例&#xff1a; if condition is False: print("条件为假") 2. True - 意义&#xff1a;布尔类型真值&#xff08;首字母大写&#xff09; - 用法示例&…...

【计算机网络】DHCP工作原理

DHCP(动态主机配置协议) Dynamic Host Configuration Protocol 基于UDP协议传输 DHCP分配IP地址的过程 &#xff08;1&#xff09;DHCP DISCOVER客户机请求 IP 地址&#xff1a; 当一个 DHCP 客户机启动时&#xff0c;客户机还没有 IP 地址&#xff0c;所以客户机要通过 DHC…...

python 原型链污染学习

复现SU的时候遇到一道python原型链污染的题&#xff0c;借此机会学一下参考&#xff1a; 【原型链污染】Python与Jshttps://blog.abdulrah33m.com/prototype-pollution-in-python/pydash原型链污染 文章目录 基础知识对父类的污染命令执行对子类的污染pydash原型链污染打污染的…...

量子计算:未来计算技术的革命性突破

在当今科技飞速发展的时代&#xff0c;量子计算正逐渐从理论走向实践&#xff0c;成为计算技术领域最具潜力的革命性突破之一。与传统计算机基于二进制的计算方式不同&#xff0c;量子计算利用量子比特&#xff08;qubit&#xff09;的叠加和纠缠特性&#xff0c;能够在处理复杂…...

Maven:Java项目构建与依赖管理工具

Maven 是什么 Maven 将项目开发过程和管理过程抽象成一个项目对象模型&#xff08;POM&#xff09;&#xff0c;本质上是一个项目管理工具。Maven 主要用于Java项目的依赖管理、编译、测试、打包和部署等操作。 Maven的核心设计围绕标准化和自动化&#xff0c;通过一系列约定和…...

内积相似系数——内积度量相似系数

内积与相似系数 内积&#xff08;Inner Product&#xff09; 内积&#xff08;Inner Product&#xff09;&#xff0c;也称为点积&#xff08;Dot Product&#xff09;或标量积&#xff0c;两个向量点积的结果是一个标量&#xff08;通常是实数或复数&#xff09;。 内积&…...

问题:md文档转换word,html,图片,excel,csv

文章目录 问题&#xff1a;md文档转换word&#xff0c;html&#xff0c;图片&#xff0c;excel&#xff0c;csv&#xff0c;ppt**主要职责****技能要求****发展方向****学习建议****薪资水平** 方案一&#xff1a;AI Markdown内容转换工具打开网站md文档转换wordmd文档转换pdfm…...

GET 和 POST 有什么区别

GET 和 POST 是 HTTP 协议中两种最常见的请求方法&#xff0c;它们在用途、安全性、数据传递方式等方面有显著的区别。以下是它们的主要区别&#xff1a; 1. 用途 • GET&#xff1a; • 用于从服务器获取资源&#xff08;数据&#xff09;。 • 是一种无状态的操作&#xf…...

AI Agent 人工智能相关公开比赛汇总

参与 AI 相关比赛是提升技术能力、接触前沿算法、积累项目经验的绝佳方式。以下是全球知名的比赛&#xff0c;以及适合不同水平选手的竞赛分类。 1. 全球知名 AI & 计算机竞赛 (1) Kaggle 竞赛&#xff08;Kaggle Competitions&#xff09; 简介&#xff1a;全球最知名的…...

Java 多线程编程之 Object.wait 方法(工作原理、高级特性、notify 方法与 notifyAll 方法)

一、wait 方法 1、基本介绍 wait 方法是 Java 中每个对象都拥有的方法&#xff0c;它继承自 Object 类 wait 方法使当前线程进入等待状态&#xff0c;直到其他线程调用该对象的 notify 方法或 notifyAll 方法 wait 方法必须在同步代码块中使用&#xff0c;否则抛出 Interrup…...

python下载m3u8格式视频

一、安装 m3u8库 pip install requests pip install requests m3u8 二、编码实现 import os import re import requests import subprocess# 下载ts文件 def down_ts_file(base_url, m3u8_url, download_dir):# 从m3u8文件中获取所有ts的分片名称信息response requests.get…...

3.30 代码随想录第三十天打卡

准备:01背包理论基础&#xff08;二维&#xff09; 1.有n个物品每个物品只有一个 2.完全背包是有n个物品每个物品有无限多个 3.多重背包是有n个物品每种物品个数各不相同 &#xff08;1&#xff09;题目描述&#xff1a; &#xff08;2&#xff09;解题思路&#xff1b; 1…...

01 相机标定与相机模型介绍

学完本文,您将了解不同相机模型分类、内参意义,及对应的应用代码模型 标定的意义 建模三维世界点投影到二维图像平面的过程。标定输出的是相机模型。 相机模型 相机模型可以解理解为投影模型 +...

鸿蒙学习手册(HarmonyOSNext_API16)_应用开发UI设计:相对布局

概述 RelativeContainer 就像个「智能拼图板」&#xff0c;帮你把界面组件像拼图一样自由组合&#xff0c;不用一层套一层地堆叠。每个组件可以直接「贴」到其他组件旁边或容器边缘&#xff0c;省去多层嵌套的麻烦&#xff0c;让复杂界面更高效。 举个接地气的例子 &#x1f3…...

关于为什么使用redis锁,不使用zk锁的原因

实际项目中&#xff0c;redis一直是最为稳定、可靠的部分&#xff0c;你根本不用担心redis本身的问题。至于ap模型的问题&#xff0c;绝大多数分布式锁只是用于避免一些极端情况的&#xff0c;若单一数据会有那么高的并发量你还加锁&#xff0c;那就要考虑这个业务场景设置的合…...

string的基本使用

C基础格式 C语言语法STL。蓝桥杯选用C11的版本。 #include <bits/stdc.h> #include <iostream> using namespace std; int main() {cout<<"Hello World!"<<endl;printf("Hello World!");return 0; } 基本数据类型 #include &l…...

论文阅读笔记——PointVLA: Injecting the 3D World into Vision-Language-Action Models

PointVLA 论文 现有的 VLA 基于 2D 视觉-语言数据表现良好但缺乏 3D 几何先验导致空间推理缺陷。传统方案&#xff1a;1&#xff09;3D->2D 投影&#xff0c;造成几何信息损失&#xff1b;2&#xff09;3D 数据集少。PointVLA 保留原有 VLA&#xff0c;提取点云特征&#xf…...

MySQL数据库精研之旅第四期:解锁库操作高阶技能

专栏&#xff1a;MySQL数据库成长记 个人主页&#xff1a;手握风云 目录 一、查看所有表 1.1. 语法 二、创建表 2.1. 语法 2.2. 示例 2.3. 表在磁盘上对应的⽂件 三、查看表结构 3.1. 语法 3.2. 示例 四、修改表 4.1. 语法 4.2. 示例 五、删除表 5.1. 语法 5.2.…...

自定义一个C语言字符串取整函数

一、字符串取整的主要思路 1、遍历每个字符&#xff1b; 2、获得0到9的字符对应的整数值&#xff1b; 3、把对应位置的十进制权重相乘&#xff1b; 4、把所有的相乘结果相加&#xff1b; 5、返回相加结果&#xff1b; 二、主要代码 // 主要是把十进制的整数字符转成十进制变量值…...

Ruby 命令行选项

Ruby 命令行选项 概述 Ruby 是一种广泛使用的编程语言,它拥有强大的命令行工具,可以帮助开发者进行各种任务。了解 Ruby 的命令行选项对于提高开发效率至关重要。本文将详细介绍 Ruby 的常用命令行选项,帮助开发者更好地利用 Ruby 的命令行功能。 Ruby 命令行选项概述 R…...