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

硅基计划6.0 JavaEE 贰 多线程八股文

1755615271516


文章目录

  • 一、常见锁策略
  • 二、synchronized锁
  • 三、CAS
    • 1. 原子类
    • 2. 伪代码解释
    • 3. 自旋锁
    • 4. CAS中的ABA问题
  • 四、JUC常见类
    • 1. callable接口
    • 2. Reentrant可重入锁
    • 3. 信号量
    • 4. CountDownLatch
    • 5. 线程安全集合类
    • 6. 多线程哈希表


一、常见锁策略

以下集中锁策略与编程语言无关,属于是通用的那种

  • 乐观锁&悲观锁:在加锁的时候我们就去预测这把锁的竞争是大还是小,如果是大并且实现原理复杂,那就是悲观锁,反之就是乐观锁
  • 重量级锁&轻量级锁:对于重量级锁来说,加锁这个操作开销大并且它容易触发线程调度,反之就是轻量级锁
  • 自旋锁&挂起等待锁:挂起等待锁是重量级锁的典型实现,当遇到锁冲突的时候,线程会进入阻塞状态,等待未来的某个时间唤醒,由于操作系统内部线程调度是随机的,开销比较大;自旋锁是轻量级锁的典型实现,遇到锁冲突时,线程会先重试获取锁,等到其他线程把锁释放了我们当前线程就可以得到锁了,并不会涉及到线程调度和CPU内核
  • 公平锁&非公平锁:我们把先来先得到锁称为公平锁,反之如果是各凭本事得到就是非公平锁
  • 可重入锁&不可重入锁:一个线程针对同一把锁,连续加锁多次不会发生死锁,就称之为可重入锁,会发生死锁就叫不可重入锁
  • 读写锁&普通互斥锁:读操作的锁叫做读锁,写入操作的锁叫做写锁,读锁与读锁之间并不会冲突,反观读锁和写锁或者是写锁和写锁之间会产生冲突

二、synchronized锁

我们这把锁是根据当前锁竞争程度来自动调整锁策略的,我们感知不到也不能干预,因此它又被称之为智能锁

底层大致实现过程

进入代码块 进入代码块        阻塞线程数量到一定程度无锁 --> 偏向锁 --> 自旋锁 --> 重量级锁

我们来说说什么是偏向锁

其实偏向锁并不是真的上锁了,只是去做个标记,开销比较小
如果使用的时候其他线程没有来竞争这把锁,就始终是一种标记状态,一直到锁释放了就解除标记了,这种就是锁消除
反之如果有其他线程竞争这把锁,在这个线程竞争这把锁想要得到这把锁之前,就把这把锁升级,因此这个竞争这把锁的线程只能进入阻塞状态,如果有更多的线程来竞争,就升级为重量级锁

我们之前说过锁的粒度,越大其实锁的粗度也就越大


三、CAS

我们在实现线程安全的时候,是通过加锁去解决的,但是现在,我们有另一种解决思路
CAS全称为compare and swap

将内存中的某个变量当前值与我期望它现在是多少进行比较
如果相等,说明从我读取这个值到现在,没有其他线程修改过它,那我就安全地更新为新值
如果不相等,说明值已经被其他线程改过,那我这次 CAS 操作就失败,可以选择重试(自旋)或放弃

image-20251106000017276

上述我们的一系列操作属于是原子性操作,内部实现了自旋锁

1. 原子类

我们在开发中使用,比如我们上一篇文章最开始的时候,对于int变量count++问题上会产生线程安全问题,因此此时我们使用原子类就可以很大程度上避免这个问题

public class Demo1 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 100; i++) {count.incrementAndGet();//相当于count++}});Thread t2 = new Thread(()->{for (int i = 0; i < 100; i++) {count.incrementAndGet();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

除了使用incrementAndGet()或者是getAndIncrement()是变量自增
还可以使用getAndDecrement()或者是decrementAndGet()是变量自减

2. 伪代码解释

private int value;
getAndIncrement(){//把内存中的值读取到寄存器中int oldValue = value;//判断value值和oldValue值是否相同,相同就触发交换,判断为true,循环进不来//执行CAS操作,把寄存器value+1位置的值和内存中的值交换//反之当其他线程穿插执行的时候,我们先不赋予值//把其他线程穿插的值再重新加载到寄存器上,while(CAS(value,oldValue,oldValue+1) != true){//再次加载值,再进行CAS判断oldValue = value;}
}

3. 自旋锁

我们之前讲得锁升级的过程,其实synchronized锁内部实现了CAS,其本质是轻量级锁
因此比起阻塞状态等待,我们还不如循环等待,开销就会小很多,下面我写个循环等待的伪代码

private Thread owner;//用于记录是哪个线程持有这把锁
public void lock(){//如果我当前锁持有对象是空的,我就把这把锁赋予到当前线程while(!CAS(this.owner,null,Thread.currentThread())){//如果当前锁对象被另一个对象持有了,我们就进入循环体内部等待//直到另一个线程释放了锁,我们再去CAS判断  }
}
public void unlock(){//解锁就是把持有锁的对象设为空this.owner = null;
}

4. CAS中的ABA问题

可能存在一种情况,一个线程把一个变量值从A改到了B,但是另一个线程又把同一个变量值从B改回了A,此时在CAS看来,这个值好像没有改过一样,因此触发ABA问题

我们来举一个转钱的例子

int oldMoney = money;
CAS(money,oldmoney,oldmoney-500)

image-20251106082806378

面对这种问题,本质上是不能让其他线程同时修改,我们可以约定一个版本号,余额可以加减,但是版本号只能加不能减,每次改动余额的时候我们都原子性的把版本号+1,我们在用CAS判断版本号是否被修改过就好

四、JUC常见类

就是在Java.Uitl.concurrent包下常见的关于线程的类

1. callable接口

这个接口类似于runnable,但是它的call方法可以直接去接收线程的返回值

public class Demo2 {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 = 0; i < 1000; i++) {sum += i;}return sum;}};//因为对于Thread类来说无法直接接收Callable类的返回值//需要一个类表示Callable的未来会接收到的结果FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//此时get方法如果我们Callable中没有执行完就进入阻塞状态//等到执行完了我们就使用get方法获取值System.out.println(futureTask.get());}
}

2. Reentrant可重入锁

这个是上古时期的手动加锁,因为那个时候synchronized还没有那么智能,因此那时候普遍使用这个类表示可重入锁,需要自己手动的加锁和解锁

同时也支持trylock,对比lock加锁不成功就进入阻塞等待状态,trylock就是加锁不成功可以主动返回或者是等待指定时间

而且,我们synchronized中的waitnotify只能随机唤醒一个,但是我们Reentrant可以搭配Condition指定唤醒哪个线程

如果我们在new ReentrantLock()参数中填入true,就表示是公平锁
原则:先来先服务,按照线程请求锁的顺序分配
实现:内部维护一个等待队列,新来的线程排队等待

public class Demo3 {public static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock locker = new ReentrantLock();//由于我们在lock和unlock之间可能存在throw异常或者是return返回//因此我们使用try-finally,在try中上锁,为了保证解锁一定会执行//我们在finally中解锁Thread t1 = new Thread(()->{for (int i = 0; i < 1000; i++) {try {locker.lock();count++;}finally{locker.unlock();}} });Thread t2 = new Thread(()->{for (int i = 0; i < 1000; i++) {try {locker.lock();count++;}finally {locker.unlock();}}});t1.start();t2.start();t1.join();t2.join();}
}

3. 信号量

类名是Semaphore,是Java对操作系统提供的机制进行了进一步的封装,本质上就是一个计数器,描述了当前系统“可用资源”的个数
最多减少到0,如果此时线程进行资源申请(P操作)就会造成阻塞,如果有其他线程释放了资源(U操作)当前线程就会从阻塞状态重新变成就绪状态

public static void main(String[] args) throws InterruptedException {//我们参数决定其初识资源个数Semaphore s = new Semaphore(2);//进行几次p操作再进行u操作s.acquire();System.out.println("获取资源");s.acquire();System.out.println("获取资源");s.release();//此时释放了资源,重新进入就绪状态System.out.println("添加了资源");s.acquire();//到这里因为资源耗尽,陷入阻塞System.out.println("再次获取到资源");}

我们还可以实现类似于“锁”的效果,我们让资源只有一个,当一个线程在执行的时候,把资源获取
此时资源就是空,其他线程只能阻塞,等到当前线程执行完毕后,再把资源添加回去,让其他线程执行

public static int count = 0;public static void main(String[] args) throws InterruptedException {Semaphore s = new Semaphore(1);Thread t1 = new Thread(()->{for (int i = 0; i < 500; i++) {try {s.acquire();} catch (InterruptedException e) {throw new RuntimeException(e);}count++;s.release();}});Thread t2 = new Thread(()->{for (int i = 0; i < 500; i++) {try {s.acquire();} catch (InterruptedException e) {throw new RuntimeException(e);}count++;s.release();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);//1000
}

因此到目前为止,解决线程安全问题我们有了几个策略

  • 避免多线程修改同一个变量
  • 使用synchronized
  • 使用ReentrantLock
  • 使用Semaphore信号量中资源值特性
  • CAS或使用原子类

4. CountDownLatch

比如我们要执行一个大任务,我们可以把大任务分成小任务,然后让每一个线程去执行这个小任务
只有当所有线程执行完自己小任务之后,这个大任务才算完成

public class Demo5 {public static void main(String[] args) throws InterruptedException {//参数表示任务数量,我们把主线程的大任务拆分成小任务CountDownLatch c = new CountDownLatch(5);for (int i = 0; i < 5; i++) {Thread t = new Thread(()->{//假设我们每个任务都要执行两秒钟try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}//两秒后完成任务,并且向c提交c.countDown();});t.start();}//我们主线程要等所有线程的小任务执行完毕//可以使用join,我们使用CountDownLatch中的方法c.await();}
}

这种我们的场景就是多线程下载内容,把一个大内容拆分成许多个小内容,我们再让子线程分别去执行一个小内容,当所有子线程内容都下载完毕后,主线程把所有内容进行整合即可

5. 线程安全集合类

我们很多类型都是线程不安全的,天生线程安全的有Vector,HashTable,Stack,String等待
但是对于线程不安全的类我们可以通过几种方式使其线程安全

  1. 添加synchroinzed关键字,之前讲过
  2. 通过Collections.synchroinzed(new ArrayList),但是不常用

接下来就是重量级嘉宾,使用copyOnWriteArrayList,即写时拷贝
在对一个变量进行修改的时候,我们先复制一份,然后在这个复制后的副本内修改值,再把修改后的副本值覆盖原来的值,避免了那种修改了但是只修改了一半的情况,即脏数据

6. 多线程哈希表

我们之前说过HashMap线程不安全,虽然HashTable天生线程安全,但是它的加锁策略有很大问题

image-20251106111319911

因此我们使用concurrentHashMap就可以避免这种情况,它是有很多把锁,针对每一个下标(即每一个下标的链表头节点)进行加锁
这样就可以避免上述那种情况,并且对每个链表头节点加锁开销不是很大,我们把这种结构称之为锁通,因此这个类又叫做哈希桶
而且对于哈希表中的size表示的键值对总数,我们使用CAS让其线程安全,避免了加锁


如果后续我们想扩容concurrentHashMap不会一次性把所有数据都拷贝到新的哈希数组
因为如果哈希数组很大的话并且每个链表长度比较长,一次性复制开销会非常大,因此concurrentHashMap同时维护新数组和旧数组,分成几个部分进行拷贝


文章可能有错误欢迎指出,这些八股文面试中经常考到

Git码云仓库链接

END♪٩(´ω`)و♪`
http://www.dtcms.com/a/578158.html

相关文章:

  • 锁相环技术及FPGA实现
  • 订单网站模板wordpress建产品目录
  • 设计网站外网制作一个网站多少钱
  • Select 服务器实战教学:从 Socket 封装到多客户端并发
  • Linux----文件系统
  • 国家允许哪几个网站做顺风车嘉兴网站建设外包公司
  • 新乡建网站个体工商户做网站
  • C# 分部类实现计算器功能
  • 怎样建设个人网站广告赚钱彩票投资理财平台网站建设
  • 代码编辑器
  • C# 中,0.1 在什么情况下不等于 0.1 ?
  • 哪块行业需要网站建设揭阳企业建站系统
  • 目前主流网站开发所用软件建筑工程公司起名
  • 【stm32协议外设篇】- NEO-6M GPS 模块
  • 内网网站开发费用泰安网签查询2023
  • 微算法科技(NASDAQ MLGO)采用动态层次管理和位置聚类技术,修改pBFT算法以提高私有区块链网络运行效率
  • 潍坊网络建站模板wordpress 指定页面nofollow
  • 从Hive on YARN到Hive on Spark
  • 创作写作-李劭卓
  • 论文分享 |Spark-TTS:用解耦语音令牌实现高效可控的语音合成
  • Spark 文本分类实战经验总结
  • 英伟达体系内关于 DGX Spark 的讨论观点整理
  • 模版型网站a站为什么会凉
  • 强软弱虚四种引用
  • [Esterel大师课] Gérard Berry:使用Esterel v7进行同步多时钟电路设计(2013)
  • 有什么学做木工的网站吗WordPress添加下载弹窗
  • 目标检测模型SSD详解与实现
  • 网站弹窗广告代码企业官方网站的作用
  • 网站建设排行山西省确诊病例最新情况
  • 线程池浅谈