多线程常见面试题
常见的锁策略
这里讨论的锁策略,不仅仅局限于 Java
乐观锁 vs 悲观锁
锁冲突: 两个线程尝试获取一把锁,一个线程能获取成功,另一个线程阻塞等待。
乐观锁: 预该场景中,不太会出现锁冲突的情况。后续做的工作会更少。
悲观锁: 预测该场景,非常容易出现锁冲突。后续做的工作会更多。
重量级锁 vs 轻量级锁
重量级锁: 加锁的开销是比较大的(花的时间多,占用系统资源多)
轻量级锁: 加锁开销比较小的,(花的时间少,占用系统资源少)
一个悲观锁,很可能是重量级锁(不绝对)。一个乐观锁,也很可能是轻量级锁(不绝对)
悲观乐观,是在加锁之前,对锁冲突概率的预测,决定工作的多少。重量轻量,是在加锁之后,考量实际的锁的开销。正是因为这样的概念存在重合,针对一个具体的锁,可能把它叫做乐观锁,也可能叫做轻量级锁。
自旋锁(Spin Lock)vs 挂起等待锁
自旋锁:是轻量级锁的一种典型实现
- 在用户态下,通过自旋的方式**(while 循环)**实现类似于加锁的效果的
- 这种锁,会消耗一定的 cpu 资源,但是可以做到最快速度拿到锁。
挂起等待锁:是重量级锁的一种典型实现
- 通过内核态,借助系统提供的锁机制。
- 当出现锁冲突的时候,使冲突的线程出现挂起**(阻塞等待)**。挂起等待不会消耗CPU
- 这种方式,消耗的 cpu 资源是更少的。也就无法保证第一时间拿到锁。
读写锁 VS 互斥锁
读写锁:把读操作加锁和写操作加锁分开了
一个事实: 多线程同时去读同一个变量,不涉及到线程安全问题。
如果两个线程, 一个线程读加锁,另一个线程也是读加锁,不会产生锁竞争。(并发执行效率更高了)
如果两个线程,一个线程写加锁,另一个线程也是写加锁,会产生锁竞争。
如果两个线程, 一个线程写加锁,另一个线程读加锁,也会产生锁竞争。
实际开发中,读操作的频率,往往比写操作,高很多。Java 标准库里,也提供了现成的读写锁。ReentrantReadWriteLock 。
- ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
加锁解锁。 - ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
行加锁解锁。
互斥锁:Synchronized 这种只有单纯的加锁解锁两个操作。
公平锁 vs 非公平锁
公平锁:是遵守先来后到的锁。B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁 。
非公平锁看起来是概率均等,但是实际上是不公平.(每个线程阻塞时间是不一样的)。
操作系统自带的锁 (pthread mutex) 属于是非公平锁。要想实现公平锁,就需要有一些额外的数据结构来支持。比如需要有办法记录每个线程的阻塞等待时间。
可重入锁 vs 不可重入锁
如果一个线程,针对一把锁,连续加锁两次,会出现死锁,就是不可重入锁; 不会出现死锁,就是可重入锁。
public synchronized void increase(){synchronized(locker){count++;}
}
1.调用方法,先针对 this 加锁. 此时假设加锁成功了
2.接下来往下执行到 代码块 中的 synchronized。此时,还是针对 this 来进行加锁。
此时就会产生锁竞争.当前 this 对象已经处于加锁状态了。此时,该线程就会阻塞,一直阻塞到锁被释放,才能有机会拿到锁。
此时,由于 this 的锁没法释放。这个代码就卡在这里了,因此这个线程就僵住了。此时就产生了死锁。
这里的关键在于,两次加锁,都是“同一个线程"。第二次尝试加锁的时候,该线程已经有了这个锁的权限了, 这个时候不应该加锁失败的,不应该阻塞等待的。
不可重入锁:这把锁不会保存,是哪个线程对它加的锁。只要它当前处于加锁状态之后,收到了"加锁”这样的请求 就会拒绝当前加锁。而不管当下的线程是哪个。就会产生死锁。
可重入锁:是会让这个锁保存,是哪个线程加上的锁。后续收到加请求之后,就会先对比一下,看看加锁的线程是不是当前持有自己这把锁的线程,这个时候就可以灵活判定了。
synchronized本身是一个可重入锁, 实际上不会产生上述的死锁情况。
死锁
死锁概念
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。多个线程被无限期阻塞,导致线程不可能正常终止。
死锁的三种典型情况:
- 一个线程,一把锁,但是是不可重入锁.该线程针对这个锁连续加锁两次,就会出现死锁
- 两个线程,两把锁.这两个线程先分别获取到一把锁,然后再同时尝试获取对方的锁。
- N 个线程 M 把锁,哲学家就餐问题。
两个线程两把锁问题:
就相当于一个在疫情时期的一个段子。健康码坏了,程序员要进去修,但是程序员不能出示健康码不能进去修,要想有健康码就得修好了才能出示。
public class ThreadDemo {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1两把锁加锁成功");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1){System.out.println("t2两把锁加锁成功");}}});t1.start();t2.start();}
}
上面代码就会出现死锁问题。每个线程都卡在了第二次加锁的过程。
如果是一个服务器程序,出现死锁。死锁的线程就僵住了,就无法继续工作了, 会对程序造成严重的影响。
N 个线程 M 把锁,哲学家就餐问题:

每个哲学家,主要要做两件事:
1.思考人生.会放下筷子
2.吃面.会拿起左手和右手的筷子,再去夹面条吃。
其他设定:
1.每个哲学家,啥时候思考人生,啥时候吃面条,都不确定的
2.每个哲学家一旦想吃面条了,就会非常固执的完成吃面条的操作。如果此时,他的筷子被别人使用了,就会阻塞等待,而且等待过程中不会放下手里已经拿着的筷子。
基于上述的模型设定,绝大部分情况下,这些哲学家都是可以很好的工作的。但是,如果出现了极端情况,就会出现死锁。比如:同一时刻,五个哲学家都想吃面,并且同时伸出 左手 拿起左边的筷子。再尝试伸右手拿右边的筷子。此时就会哪个哲学家都不会吃上面条了,这里五个哲学家无根筷子相当于5个线程5把锁。
避免死锁
死锁产生的必要条件:
- 互斥使用:一个线程获取到一把锁之后,别的线程不能获取到这个锁
- 实际使用的锁,一般都是互斥的(锁的基本特性)
- 不可抢占锁: 只能是被持有者主动释放,而不能是被其他线程直接抢走
- 也是锁的基本的特性
- 请求和保持: 一个线程去尝试获取多把锁,在请求获取第二把锁的过程中,会保持对第一把锁的获取状态。
- 取决于代码结构(很可能会影响到需求)
- 循环等待: t1 尝试获取 locker2,需要 等待 t2 执行完,释放 locker2。t2 尝试获取 locker1,需要 等待 t1 执行完,释放 locker1。
- 取决于代码结构
缺一不可,只要能够破坏其中的任意一个条件,都可以避免出现死锁。
解决死锁问题的最关键要点:破除循环等待。
破除循环等待:针对锁进行编号。并且规定加锁的顺序。比如,约定,每个线程如果要获取多把锁,必须先获取 编号小的锁,后获取编号大的锁。只要所有线程加锁的顺序,都严格遵守上述顺序,就一定不会出现环等待。

针对上面死锁代码进行加锁编号,来解决死锁问题:
public class ThreadDemo {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t1两把锁加锁成功");}}});Thread t2 = new Thread(()->{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2){System.out.println("t2两把锁加锁成功");}}});t1.start();t2.start();}
}
Synchronized 原理
Synchronized的锁策略
synchronized 具体是采用了哪些锁策略呢?
- 1.synchronized 既是悲观锁, 也是乐观锁
- 2.synchronized 既是重量级锁,也是轻量级锁.(自适应)
- 3.synchronized 重量级锁部分是基于系统的互斥锁实现的; 轻量级锁部分是基于自旋锁实现的
- 4.synchronized 是非公平锁(不会遵守先来后到 锁释放之后,哪个线程拿到锁,各凭本事)
- 5.synchronized 是可重入锁.(内部会记录哪个线程拿到了锁,记录引用计数)
- 6.synchronized 不是读写锁,是互斥锁。
synchronized 加锁过程
代码中写了一个 synchronized 之后,这里可能会产生一系列的“自适应的过程”,锁升级(锁膨胀)。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁:不是真的加锁,而只是做了一个”标记“。如果有别的线程来竞争锁了,才会真的加锁。如果没有别的线程竞争,就自始至终都不会真的加锁了。**加锁本身,有一定开销。能不加,就不加。非得是有人来竞争了,才会真的加锁。**偏向锁在没有其他人竞争的时候,就仅仅是一个简单的标记(非常轻量)。一旦有别的线程尝试进行加锁,就会立即把偏向锁,升级成真正加锁的状态,让别人只能阻塞等待。
轻量级锁:sychronized 通过自旋锁的方式来实现轻量级锁。我这边把锁占据了,另一个线程就会按照自旋的方式,来反复查询当前的锁的状态是不是被释放了。但是,后续,如果竞争这把锁的线程越来越多了(锁冲突更激烈了),从轻量级锁,升级成重量级锁。
轻量级锁的操作是比较消耗 CPU 的。 如果能够比较快速的拿到锁,多消耗点 CPU 也不亏。但是,随着竞争更加激烈,即使前一个线程释放锁 ,也不一定能拿到锁,啥时候能拿到,时间可能会比较久了。
synchronized 的优化操作
锁消除:编译器,会智能的判定,当前这个代码,是否有必要加锁。如果,你写了加锁,但是实际上没有必要加锁,就会把加锁操作自动删除掉。
锁粗化:关于"锁的粒度",如果加锁操作里包含的实际要执行的代码越多,就认为锁的粒度越粗。一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化 。

有的时候,希望锁的粒度小比较好,并发程度更高。有的时候,也希望锁的粒度大比较好,因为加锁解锁本身也有开销。
CAS
CAS的概念
CAS: 全称Compare and swap,字面意思:”比较并交换“。能够比较和交换 某个寄存器 中的值 和 内存 中的值,看是否相等。如果相等,则把另外一个寄存器中的值和内存进行交换。
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;//此处,严格的说,是把 address 内存的值,和 swapValue 寄存器里的值, 进行交换。//但是一般我们重点关注的是内存中的值。//寄存器往往作为保存临时数据的方式,这里的值是啥,很多时候就忽略了。return true;}return false;
}
address:内存地址
expectValue, swapValue:寄存器中的值
上面一段逻辑,是通过一条 cpu 指令完成的(原子的)。这个就给我们编写线程安全代码,打开了新世界的大门。基于 CAS 又能衍生出一套"无锁编程“。但是CAS 的使用范围具有一定局限性的。
CAS的实现是:硬件予以了支持,软件层面才能做到 。
CAS的应用
1. 实现原子类
比如,多线程针对一个 count 变量进行 ++,在java 标准库中基于CAS,已经提供了一组原子类。
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的。AtomicBoolean,AtomicInteger,AtomicIntegerArray,AtomicLong,AtomicReference,AtomicStampedReference
以 AtomicInteger 举例,常见方法有 :
-
addAndGet(int delta); 相当于 i += delta;
-
getAndIncrement 相当于 i++ 操作。
-
incrementAndGet 相当于 ++i 操作。
-
getAndDecrement 相当于 i-- 操作。
-
decrementAndGet 相当于 --i 操作。
public class ThreadDemo26 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 5000; i++) {//Java 不像 C++ Python 能支持运算符重载,这里必须通过调用方法的方式来完成自增count.getAndIncrement();}});Thread t2 = new Thread(()->{for (int i = 0; i < 5000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}
上述的原子类,就是基于 CAS 来实现的。
伪代码实现:
class AtomicInteger {private int value;//很可能有个别的线程穿插在这俩代码之间,把 value 给改.public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}
oldValue:也可以是寄存器中的值,由于以往学过的 C/Java 里头,并没有啥办法定义一个“寄存器”的变量。

这里的比较value和oldValue相等,其实就是在检查当前 value 是不是变了。是不是被别的线程穿插进来做出修改了。进一步就发现了当前的 ++ 操作不是一气呵成的原子操作了。一旦发现出现其他线程穿插的情况,立即重新读取内存的值准备下一次尝试。
加锁保证线程安全: 通过锁,强制避免出现穿插。
原子类/CAS 保证线程安全: 借助 CAS 来识别当前是否出现其他线程"穿插”的情况。如果没穿插,此时直接修改 就是安全的。如果出现穿插了,就重新读取内存中的最新的值,再次尝试修改。
2. 实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权。
自旋锁伪代码:
public class SpinLock {private Thread owner = null;//此处使用 owner 表示当前是哪个线程持有的这把锁.null 解锁状态public void lock(){// 通过 CAS 看当前锁是否被某个线程持有.// 如果这个锁已经被别的线程持有, 那么就自旋等待.// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.while(!CAS(this.owner, null, Thread.currentThread())){//Thread.currentThread()获取当前线程引用//哪个线程调用 lock,这里得到的结果就是哪个线程的引用}//当该锁已经处于加锁状态,这里就会返回 false, //CAS 不会进行实际的交换操作.接下来循环条件成立,继续进入下一轮循环}public void unlock (){this.owner = null;}
}
CAS 的 ABA 问题
CAS 关键要点,是比较 寄存器1 和 内存 的值。通过这里的是否相等,来判定 内存的值 是否发生了改变。如果内存的值变了,存在其他线程进行了修改。如果内存的值没变,没有别的线程修改,接下来进行的修改就是安全的。
ABA 的问题: 另一个线程,把 变量的值从 A -> B,又从 B -> A。此时本线程区分不了,这个值是始终没变,还是出现变化又回来了的情况。
大部分情况下,就算是出现 ABA 问题,也没啥太大影响。但是如果遇到一些极端的场景可能会出现问题:
账户 100 ,希望取款50,还剩50。假设出现极端问题:按第一下取款的时候,卡了一下, 我又按了一下。产生了两个“取款”请求,ATM 使用两个线程来处理这俩请求。假设按照 CAS 的方式进行取款,每个线程这样操作:
- 读取账户余额.放到变量 M 中。
- 使用 CAS 判定当前实际余额是否还是 M。如果是,就把实际余额修改成 M-50。如果不是,就放弃当前操作(操作失败)。

上面这个ABA问题属于非常巧合的情况,取款的时候卡了 + 碰巧这个时候有人给你转了50
虽然上述操作,概率比较小,也需要去考虑。ABA问题的解决方式:
ABA 问题,CAS 基本的思路是 没有问题 的,但是主要是修改操作能够进行反复改变,就容易让咱们 cas 的判定失效。CAS 判定的是“值相同”,实际上期望的是“值没有变化过"。比如约定,值只能单向变化(比如只能增长,不能减小)。虽余额不能只增张不减少,但是衡量余额是否改变的标准可以是看版本号。给账户余额安排一个 其他属性版本号(只增加,不减少)。使用 CAS 判定版本号,如果版本号相同,则数据一定是没有修改过的,如果数据修改过,版本号一定要增加。
JUC(java.util.concurrent) 的常见类
juc中的类是为了并发编程准备的。java官方文档
Callable interface
也是一种创建线程的方式
Runnable 能表示一个任务 (run 方法),返回 void
Callable 也能表示一个任务 (call 方法),返回一个具体的值,类型可以通过泛型参数来指定(Object)。
如果进行多线程操作,如果你只是关心多线程执行的过程,使用 Runnable 即可如。果是关心多线程的计算结果,使用 Callable 更合适。
通过多线程的方式计算一个公式,比如创建一个线程,让这个线程计算 1 + 2 + 3 +…+ 1000,使用Callable解决更合适。
- 使用 Callable 不能直接作为 Thread 的构造方法参数
- 借助FutureTask 来作为Thread的构造方法参数
public class ThreadDemo {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;}};//使用 Callable 不能直接作为 Thread 的构造方法参数FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//获取 call 方法的返结果get ,类似于join 一样, 如果 call 方法没执行完,会阻塞等待Integer result = futureTask.get();System.out.println(result);}
}
ReentrantLock
可重入锁,这个锁 没有 synchronized 那么常用,但是也是一个可选的加锁的组件。这个锁在使用上更接近于 C++ 里的锁。
- lock() 加锁
- unlock() 解锁
分开操作,就容易出现unlock 调用不到的情况,容易遗漏。比如,中间 return / 抛出异常了。ReentrantLock 具有一些特点,是 synchronized 不具备的功能(优势):
-
提供了一个 tryLock 方法进行加锁
- 对于 lock 操作,如果加锁不成功,就会阻塞等待(死等)
- 对于 tryLock,如果加锁失败,直接返回 false/也可以设定等待时间。
- tryLock 给加锁操作提供了更多的可操作空间。
-
ReentrantLock 有两种模式。可以工作在公平锁状态下,也可以工作在非公平锁的状态下。
- 构造方法中通过参数设定的 公平/非公平模式。
-
ReentrantLock 也有等待通知机制,搭配 Condition 这样的类来完成这里的等待通知。要比 wait notify 功能更强
虽然ReentrantLock有上述这些优点,但是 ReentrantLock 劣势也很明显(比较致命),unlock 容易遗漏使用 finally 来执行 unlock。
synchronized 锁对象是任意对象。ReentrantLock 锁对象就是自己本身。如果你多个线程针对不同的 ReentrantLock 调用 lock 方法,此时是不会产生锁竞争的。实际开发中,进行多线程开发,用到锁还是首选 synchronized。
原子类
原子类的应用场景:
计数请求:播放量,点赞量,投币量,转发量,收藏量。同一个视频,有很多人都在同时的播放/点赞/收藏
统计效果:
统计出现错误的请求数目。—> 使用原子类,记录出错的请求的数目。—> 另外写一个监控服务器,获取到线上服务器的这些错误计数,并且以曲线图的方式绘制到页面上。
某次发布程序之后,发现,突然这里的错误数大幅度上升,说明你这个新版本代码大概率存在 bug。
统计收到的请求总数(衡量服务器的压力)。统计每个请求的响应时间 => 平均的响应时间(衡量服务器的运行效率)。
最低 1% 的响应时间是多少(1% low 帧)。线上服务器通过这些统计内容,进行简单计数 =>实现监控服务器,获取/统计/展示/报警。
信号量 Semaphore
Semaphore 是并发编程中的一个重要的概念/组件。准确来说,Semaphore 是一个计数器(变量),描述了**"可用资源"的个数**。描述的是,当前这个线程,是否**“有临界资源可以用“**。
- P 操作:申请了一个可用资源 - 1。accquire (申请)
- V 操作:释放了一个可用资源 +1。release (释放)
当计数器数值为 0 的时候,继续进行 P 操作,就会阻塞等待,一直等待到其他线程执行了 V 操作,释放了一个空闲
资源为止。锁,本质上是一个特殊的信号量(里面的数值,非 0 即 1二元信号量)。信号量要比锁更广义,不仅仅可以描述一个资源,还可以描述 N 个资源。虽然概念上更广泛,实际开发中,还是锁更多一些(二元信号量的场景是更常见的)。
//信号量
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {// 构造方法中, 就可以用来指定计数器的初始值.Semaphore semaphore = new Semaphore(4);semaphore.acquire();//计数器-1System.out.println("执行p操作");semaphore.acquire();//计数器-1System.out.println("执行p操作");semaphore.acquire();//计数器-1System.out.println("执行p操作");semaphore.acquire();//计数器-1System.out.println("执行p操作");semaphore.acquire();//计数器-1System.out.println("执行p操作");}
}
CountDownLatch
针对特定场景一个组件。同时等待 N 个任务执行结束。
下载某个东西:有的时候,下载一个比较大的文件,比较慢(慢不是因为你家里的网速限制,往往是人家服务器这边的限制)。有一些多线程下载器”,把一个大的文件,拆分成多个小的部分,使用多个线程分别下载。每个线程负责下载一部分,每个线程分别是一个网络连接。就会大幅度提高下载速度。假设,分成 10个线程,10个部分来下载。 10个部分都下载完了,整体才算完成。
//CountDownLatch
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {// 构造方法中, 指定创建几个任务.CountDownLatch countDownLatch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int id = i;Thread t = new Thread(()-> {System.out.println("线程" + id + "开始工作");try {// 使用 sleep 代指某些耗时操作, 比如下载.Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程" + id + "结束工作");// 每个任务执行结束这里, 调用一下方法// 把 10 个线程想象成短跑比赛的 10 个运动员. countDown 就是运动员撞线了.countDownLatch.countDown();});t.start();}// 主线程如何知道上述所有的任务都完成了呢??// 难道要在主线程中调用 10 次 join 嘛?// 万一要是任务结束, 但是线程不需要结束, join 不就也不行了嘛。// 主线程中可以使用 countDownLatch 负责等待任务结束.// a => all 等待所有任务结束. 当调用 countDown 次数 < 初始设置的次数, await 就会阻塞.countDownLatch.await();System.out.println("多个线程的所有任务都执行完毕了!!");}
}
线程安全的集合类
多个线程同时操作这个集合类,不会会产生问题就是线程安全的。
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.
在关键的方法中,使用了 synchronized。Vector 和 HashTable 属于是 Java 上古时期,搞出来的集合类。加了锁,不一定就线程安全。不加锁也不一定就线程不安全 => 要具体问题具体分析。
虽然 get 和 set 方法加了 synchronized ,但是如果不能正确使用,也可能会出现线程安全问题:
- 如果是多个线程,并发执行 set 操作,由于 synchronized 限制,是线程安全。
- 如果多个线程进行一些更复杂的操作,比如判定 get 的值是 xxx,再进行 set,可能会线程不安全。

即使把这里的 get 和 set 分别进行加锁。如果不能正确的使用,也可能产生线程安全问题。考虑到实际的逻辑中,哪些代码是要作为一个整体的(原子的)。
线程安全下使用ArrayList
Collections.synchronizedList(new ArrayList);
ArrayList 本身没有使用 synchronized。但是你又不想自己加锁,就可以使用上面这个东西,相当于让 ArrayList 像 Vector 一样工作。(很少会用)
使用 CopyOnWriteArrayList 写时复制
多个线程同时修改同一个变量,如果多个线程修改不同变量,就会安全了。
如果多线程去读取,本身就不会有任何线程安全问。一旦有线程修改,就会把自身复制一份。尤其是修改比较耗时的话,其他线程还是旧的数据上读取。一旦修改完成,使用新的 ArrayList 替换目的 ArrayList (本质上就是一个引用的重新赋值速度极快,并且又是原子的)
这个过程中,没有引入任何的加锁操作。使用了创建副本 => 修改副本 => 使用副本替换。
线程安全下使用HashMap
ConcurrentHashMap 线程安全的 hash 表。
- HashTable 是在方法上直接加上 synchronized,就相当于针对 this 加锁。
如果两个修改操作,是针对两个不同的链表进行修改,不会存在线程安全问题。既然这里没有线程安全问题,但是锁又不能完全不加,因为两个修改可能在同一个链表中同一个位置进行插入操作。
为了解决上面的问题:给每个链表都加一把锁。
一个hash表上面的链表个数这么多,两个线程正好在同时操作同一个链表的概率本身就是比较低的,整体锁的开销就大大降低了。由于 synchronized 随便拿个对象都可以用来加锁,就可以简单的使用每个链表的头结点,作为锁对象即可。
ConcurrentHashMap 改进:
- [核心] 减小了锁的粒度,每个链表有一把锁。大部分情况下都不会涉及到锁冲突。
- 广泛使用了 CAS 操作(比如size++)
- 写操作进行了加锁(链表级),读操作,不加锁了。
- 针对扩容操作进行了优化,浙进式扩容。
HashTable 一旦触发扩容, 就会立即的一口气的完成所有元素的搬运,这个过程相当耗时。大部分请求都很顺畅,突然某个请求就卡了比较久。化整为零,当需要进行扩容的时候,会创建出另一个更大的数组,然后把旧的数组上的数据逐渐的往新的数组上搬运。会出现一段时间,旧数组和新数组同时存在。
- 新增元素,往新数组上插入。
- 删除元素,把旧数组的元素给删掉即可。
- 查找元素,新数组旧数组都得查找。
- 修改元素,统一把这个元素给搞到新数组上。
与此同时,每个操作都会触发一定程度搬运。每次搬运一点,就可以保证整体的时间不是很长。积少成多之后,逐渐完成搬运了,也就可以把之前的旧数组彻底销毁了。
介绍下 ConcurrentHashMap的锁分段技术?
Java 8 之前,ConcurrentHashMap 是使用分段锁,从 Java 8 开始,就是每个链表自己一把锁了。
相关文章:
多线程常见面试题
常见的锁策略 这里讨论的锁策略,不仅仅局限于 Java 乐观锁 vs 悲观锁 锁冲突: 两个线程尝试获取一把锁,一个线程能获取成功,另一个线程阻塞等待。 乐观锁: 预该场景中,不太会出现锁冲突的情况。后续做的工作会更少。 悲观锁: 预测该场景,非常容易出现锁冲突。后…...
Java接收json参数
JSON 并不是唯一能够实现在互联网中传输数据的方式,除此之外还有一种 XML 格式。JSON 和 XML 能够执行许多相同的任务,那么我们为什么要使用 JSON,而不是 XML 呢? 之所以使用 JSON,最主要的原因是 JavaScript。众所周知…...
赤峰100吨每天医院污水处理设备产品特点
赤峰100吨每天医院污水处理设备产品特点 设备调试要求: 1、要清洗水池内所有的赃物、杂物。 2、对水泵及空压机等需要润滑部位进行加油滑。 3、通电源,启动水泵,检查转向是否与箭头所标方向一致。用水动控制启动空压机,检查空压机…...
nodejs+vue+elementui健身房教练预约管理系统nt5mp
运用新技术,构建了以vue.js为基础的私人健身和教练预约管理信息化管理体系。根据需求分析结果进行了系统的设计,并将其划分为管理员,教练和用户三种角色:主要功能包括首页,个人中心,用户管理,教…...
视频分割合并工具说明
使用说明书:视频分割合并工具 欢迎使用视频生成工具!本工具旨在帮助您将视频文件按照指定的规则分割并合并,以生成您所需的视频。 本程序还自带提高分辨率1920:1080,以及增加10db声音的功能 软件下载地址 https://github.com/c…...
2023java面试深入探析Nginx的处理流程
推荐阅读 AI文本 OCR识别最佳实践 AI Gamma一键生成PPT工具直达链接 玩转cloud Studio 在线编码神器 玩转 GPU AI绘画、AI讲话、翻译,GPU点亮AI想象空间 资源分享 史上最全文档AI绘画stablediffusion资料分享 「java、python面试题」来自UC网盘app分享,打开手…...
Java的锁大全
Java的锁 各种锁的类型 乐观锁 VS 悲观锁 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。 先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数…...
Leetcode80. 删除有序数组中的重复项 II
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 class Solu…...
电脑显示“Operating System not found”该怎么办?
“Operating System not found”是一种常见的电脑错误提示,这类错误会导致你无法成功启动Windows。那么电脑显示“Operating System not found”该怎么办呢? 方法1. 检查硬盘 首先,您可以测试硬盘是否存在问题。为此,您可以采取以…...
简析SCTP开发指南
目录 前言一、SCTP基本概念二、SCTP开发步骤1. **环境配置**:2. **建立Socket**:3. **绑定和监听**:4. **接收和发送数据**:5. **关闭连接**: 三、 C语言实现SCTP3.1SCTP客户端代码:3.2 SCTP服务器端代码&a…...
把Android手机变成电脑摄像头
一、使用 DroidCam 使用 DroidCam,你可以将手机作为电脑摄像头和麦克风。一则省钱,二则可以在紧急情况下使用,比如要在电脑端参加一个紧急会议,但电脑却没有摄像头和麦克风。 DroidCam 的安卓端分为免费的 DroidCam 版和收费的 …...
Linux线程篇(中)
有了之前对线程的初步了解我们学习了什么是线程,线程的原理及其控制。这篇文章将继续讲解关于线程的内容以及重要的知识点。 线程的优缺点: 线程的缺点 在这里我们来谈一谈线程健壮性: 首先我们先思考一个问题,如果一个线程出现…...
深度学习优化入门:Momentum、RMSProp 和 Adam
目录 深度学习优化入门:Momentum、RMSProp 和 Adam 病态曲率 1牛顿法 2 Momentum:动量 3Adam 深度学习优化入门:Momentum、RMSProp 和 Adam 本文,我们讨论一个困扰神经网络训练的问题,病态曲率。 虽然局部极小值和鞍点会阻碍…...
LeetCode 面试题 01.09. 字符串轮转
文章目录 一、题目二、C# 题解 一、题目 字符串轮转。给定两个字符串 s1 和 s2,请编写代码检查 s2 是否为 s1 旋转而成(比如,waterbottle 是 erbottlewat 旋转后的字符串)。 点击此处跳转题目。 示例1: 输入:s1 “wa…...
系统上线安全测评需要做哪些内容?
电力信息系统、航空航天、交通运输、银行金融、地图绘画、政府官网等系统再正式上线前需要做安全测试。避免造成数据泄露从而引起的各种严重问题。 那么系统上线前需要做哪些测试内容呢?下面由我给大家介绍 1、安全机制检测-应用安全 身份鉴别 登录控制模块 应提供…...
vue 中 axios 的安装及使用
vue 中 axios 的安装及使用 1. axios 安装2. axios使用 1. axios 安装 首先,打开当前的项目终端,输入 npm install axios --save-dev验证是否安装成功,检查项目根目录下的 package.json,其中的 devDependencies 里面会多出一个axios及其版本…...
数据结构——线性数据结构(数组,链表,栈,队列)
文章目录 1. 数组2. 链表2.1. 链表简介2.2. 链表分类2.2.1. 单链表2.2.2. 循环链表2.2.3. 双向链表2.2.4. 双向循环链表 2.3. 应用场景2.4. 数组 vs 链表 3. 栈3.1. 栈简介3.2. 栈的常见应用常见应用场景3.2.1. 实现浏览器的回退和前进功能3.2.2. 检查符号是否成对出现3.2.3. 反…...
多态(C++)
多态 一、初识多态概念“登场”1>. 多态的构成条件2>. 虚函数3>. 虚函数重写(覆盖)4>. 虚函数重写的两个例外1. 协变 一 基类和派生类虚函数返回值类型不同2. 析构函数重写(基类和派生类析构函数名不同) 小结 二、延伸…...
算法leetcode|73. 矩阵置零(rust重拳出击)
文章目录 73. 矩阵置零:样例 1:样例 2:提示:进阶: 分析:题解:rust:go:c:python:java: 73. 矩阵置零: 给定一个 m x n 的矩…...
axios 二次封装
axios 二次封装 基本上每一个项目开发,都必须要二次封装 axios。主要是为了减少重复性工作,不可能每一次发起新请求时,都要重新配置请求域名、请求头 Content-Type、Token 等信息。所以需要把公用的部分都封装成一个函数,每次调用…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
