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

Java 多线程进阶(四)-- 锁策略,CAS,synchronized的原理,JUC当中常见的类

文章目录

  • 多线程(进阶)
    • 常见的锁策略
      • 乐观锁和悲观锁
      • 重量级锁和轻量级锁
      • 自旋锁和挂起等待锁
      • 读写锁
      • 可重入锁和不可重入锁
      • 公平锁和非公平锁
    • CAS
      • CAS实现的原子性
      • CAS实现自旋锁
      • ABA问题
  • synchronized的原理
    • 锁升级
    • 锁消除
    • 锁粗化
  • JUC(java.util.concurrent)中常见的类
    • Callable接口
    • ReentrantLock
    • 信号量Semaphore
    • CountDownLatch
    • 线程安全的集合类

多线程(进阶)

常见的锁策略

  1. 锁,不仅仅是synchronized这种锁,还有其他种类的锁
  2. 锁策略:锁的一种特性,指的是一类锁,不是一种具体的锁

乐观锁和悲观锁

  1. 乐观锁和悲观锁:是对后续锁冲突是否激烈给出的预测
  2. 乐观锁:如果预测接下来锁冲突的概率不大,就可以少做一些工作
  3. 悲观锁:如果预测接下来锁冲突的概率很大,就要多做一些工作

重量级锁和轻量级锁

在这里插入图片描述

自旋锁和挂起等待锁

在这里插入图片描述
乐观锁通常是轻量级锁
悲观锁通常是重量级锁
这并不是绝对的,也有特殊情况的

读写锁

  1. 数据库中也出现了读加锁和写加锁

在这里插入图片描述
2. 读加锁:读的时候,能读,但是不能写
写加锁:写的时候,不能读,也不能写
在这里插入图片描述

可重入锁和不可重入锁

  1. 如果一个线程对同一把锁,连续进行加锁两次,不出现死锁就是可重入锁,出现死锁就是不可重入锁

公平锁和非公平锁

在这里插入图片描述
在这里插入图片描述

CAS

  1. Compare and swap:比较和交换
    比较和交换的是寄存器和内存中的值
    例子:
    在这里插入图片描述
    CAS的伪代码(逻辑):
    在这里插入图片描述
  2. CAS其实是一个cpu指令(一个cpu指令就可以完成上述的比较和交换的逻辑),单个cpu指令是原子的,一定程度上就可以代替加锁,为线程安全问题引入了新思路
  3. 基于CAS实现线程安全的方式也称为无锁编程

优点:保证了线程安全,同时避免了阻塞(效率提高了)
缺点:代码会更复杂,不好理解.
只能够适合一些特定的场景,没有锁的普适性

在这里插入图片描述

CAS实现的原子性

在这里插入图片描述在这里插入图片描述
举个自增操作的例子:
在这里插入图片描述
自增产生线程不安全的原因是穿插执行了
加锁是通过阻塞的方式阻止了穿插执行,而CAS是通过重试的方式阻止了穿插执行
CAP这里循环判定,如果值是有变化了,就是存在其他线程在这里穿插执行了,所以这里更新了一下值

CAS实现自旋锁

  1. 纯用户态实现的自旋锁,适合比较乐观的场景
    在这里插入图片描述
    在这里插入图片描述

ABA问题

在这里插入图片描述

  1. ABA问题可能会产生bug
    在这里插入图片描述
  2. ABA问题通常情况下不会有bug,但是极端情况下就不好说了
    例子:
    这个时候是执行正确的,
    在这里插入图片描述
    但是此时来了一个t3线程,我实际上要取500,但是这里扣了1000
    在这里插入图片描述
  3. 如何解决ABA问题呢?
    在这里插入图片描述
  4. 小结:面试中容易考到
    在这里插入图片描述

synchronized的原理

  1. synchronized背后的一些问题(面试中常见的问题)
    在这里插入图片描述
  2. 锁的变化:
    在这里插入图片描述

锁升级

偏向锁的例子:
在这里插入图片描述
偏向锁的核心思想,就是’’ 懒汉模式 ‘’ 的另一种体现,能不加锁就不加锁,加锁就意味着有开销

偏向锁,如果没有人竞争这个锁,synchronized就标记一下,不加锁,就加上一个标记,这时如果要解锁也会更加高效,后续如果有锁竞争了就立马加上锁,也保证了线程安全问题

如果出现一次锁冲突就升级为自旋锁,如果遇到更多的冲突就再次升级为重量级锁

  1. 为什么要有锁升级呢?
    为了让性能和线程安全之间更好地权衡

锁消除

  1. 锁消除也是一种编译器的优化手段(在编译过程中的消除锁
  2. 编译器会自动对你写的加锁代码做出判定,如果编译器觉得你这个场景不需要加锁,就会把你写的synchronized给优化掉
    在这里插入图片描述

锁粗化

  1. 锁的粒度细,需要每次都加锁和解锁
  2. 锁的粒度粗,只需要一次加锁和解锁

在这里插入图片描述

JUC(java.util.concurrent)中常见的类

Callable接口

  1. Callable接口:可以返回结果,对比Runnable来说,Runnable并不返回结果
    在这里插入图片描述
  2. Callable的实现方法
    让Callable计算结果更加方便
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo30 {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;}};// 把任务放到线程中执行FutureTask<Integer> futureTask = new FutureTask<>(callable);// 无法直接把callable放入Thread的构造方法中Thread t = new Thread(futureTask);t.start();// 这里的get能够获取到call中的返回结果// 因为是并发执行,执行到线程中的get方法时,t线程还没有执行完// get就会阻塞等待System.out.println(futureTask.get());}
}

在这里插入图片描述
3. 创建线程的方法:
在这里插入图片描述

ReentrantLock

  1. ReentrantLock:也是可重入锁,使用效果上和synchronized效果是相似的
    在这里插入图片描述
  2. 优势:
    ReentranLock有两种加锁的方式:lock和tryLock
    lock是加锁失败就一直阻塞等待
    tryLock是加锁失败,就放弃加锁
    tryLock就有更多的操作空间了

ReentranLock提供了公平锁的实现(默认情况下是非公平锁)

在这里插入图片描述
3. 我们应该首选synchronized,因为ReentrantLock使用起来更复杂,尤其是容易忘记unlock,另外synchronized有一系列的优化手段

信号量Semaphore

  1. 信号量就是一个计数器,描述了可用资源的个数
    每次申请一个可用资源,计数器都-1(P操作)
    每次释放一个可用资源,计数器都+1(V操作)
    (这里的+1和-1都是原子的)

例子:有多少个车位,这里的车位就是一种信号量

在这里插入图片描述
2. 锁就是二元信号量
在这里插入图片描述
这样信号量就可以表示锁
而锁不能代替信号量
锁是特殊的信号量

  1. 信号量的使用
    如果遇到需要申请资源的场景可以使用信号量
import java.util.concurrent.Semaphore;public class Demo32 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(4);semaphore.acquire();System.out.println("P 操作!");semaphore.acquire();System.out.println("P 操作!");semaphore.acquire();System.out.println("P 操作!");semaphore.acquire();System.out.println("P 操作!");// 最后一个阻塞等待semaphore.acquire();System.out.println("P 操作!");semaphore.release();}
}

CountDownLatch

  1. 用来判断当前任务是否都完成了
    在这里插入图片描述
  2. CountDownLatch的两个方法:
    在这里插入图片描述
  3. CountDownLatch的使用:把大的任务拆分成多个小任务
import java.util.concurrent.CountDownLatch;public class Demo33 {public static void main(String[] args) throws InterruptedException {// 10个选手参赛,await就会在10次调用完countDown之后才能继续执行CountDownLatch count = new CountDownLatch(10);for(int i = 0;i < 9;i++){int id = i;Thread t = new Thread(()->{System.out.println("Thread " + id);try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}// 表示当前这个任务已经执行完毕了count.countDown();});t.start();}// 等待所有任务执行完毕,才能往下执行// a -> allcount.await();System.out.println("所有任务都完成了!");}
}

线程安全的集合类

多线程使用ArrayList

  1. synchronizedList:相当于是在ArrayList外面套了一个synchronized
    在这里插入图片描述
    2.CopyOnWriteArrayList:写时拷贝

在这里插入图片描述

写时拷贝的使用场景:
在这里插入图片描述
3. 多线程使用队列

在这里插入图片描述
4. 多线程使用哈希表

在这里插入图片描述
在这里插入图片描述
ConcurrentHashMap的改进的方面:
1.ConcurrentHashMap最核心的改进(在HashMap的基础上进行的改进)就是把全局的一把大锁,该进成了每一个链表是一把独立小锁,这样大大地降低了锁冲突的概率

一个哈希表中有很多这样的链表,两个线程刚好同时访问一个链表的概率就比较小

在这里插入图片描述
对第3点的补充:
写和写之间是要进行加锁的

针对第一点,每个链表都是一把小锁,需要我们把每个链表的头节点作为一个锁对象,因为synchronized可以把任何对象作为锁对象
在这里插入图片描述

  1. ConcurrentHashMap针对扩容操作做出了单独的优化

缺点:
在这里插入图片描述
解决方法:一旦要扩容,确实是要搬运的,不是一次操作中搬运全部,而是分成多次来搬运。每次只搬运一部分数据,避免了这单次操作太卡顿

ConcurrentHashMap和HashMap基本使用上是完全一样的,只不过对于加锁的方式改变了,还有一些对HashMap的优化


文章转载自:

http://Ek7Qi3ly.ppLxd.cn
http://ioyZ3BXj.ppLxd.cn
http://lRa0kLcV.ppLxd.cn
http://FT2cQGuC.ppLxd.cn
http://wPYr7xoX.ppLxd.cn
http://KYkKYgAf.ppLxd.cn
http://L9rR2eTS.ppLxd.cn
http://UGByTK6Q.ppLxd.cn
http://VpMsLvjq.ppLxd.cn
http://EV1gNTlB.ppLxd.cn
http://vTc1KlGj.ppLxd.cn
http://38exkOsB.ppLxd.cn
http://ITH7uIBd.ppLxd.cn
http://cBFWPWlh.ppLxd.cn
http://9Y2U6BQv.ppLxd.cn
http://xrSqf9Fw.ppLxd.cn
http://bB2LrCBb.ppLxd.cn
http://8TLNEKbA.ppLxd.cn
http://er1O8Qwv.ppLxd.cn
http://SnVB0TxR.ppLxd.cn
http://3etGbDvs.ppLxd.cn
http://HNFAlYEK.ppLxd.cn
http://EHREI27O.ppLxd.cn
http://bacLjwMe.ppLxd.cn
http://EiJG9VSP.ppLxd.cn
http://hVp8tKaR.ppLxd.cn
http://TJWNb6TF.ppLxd.cn
http://K3TDAUm3.ppLxd.cn
http://B6ovQL2l.ppLxd.cn
http://L1YwwvM9.ppLxd.cn
http://www.dtcms.com/a/381559.html

相关文章:

  • 从ENIAC到Linux:计算机技术与商业模式的协同演进
  • UE5版本Windows构建pc平台报错googletest的问题记录
  • 【LeetCode】杨辉三角,轮转数组,洗牌算法
  • 5.Three.js 学习(基础+实践)
  • 在 React 中如何使用 useMemo 和 useCallback 优化性能?
  • C++20多线程新特性:更安全高效的并发编程
  • 结构光三维重建原理详解(1)
  • window显示驱动开发—视频呈现网络简介
  • Vision Transformer (ViT) :Transformer在computer vision领域的应用(二)
  • 计算机网络的基本概念-2
  • 计算机视觉----opencv实战----指纹识别的案例
  • 【操作系统核心知识梳理】线程(Thread)重点与易错点全面总结
  • JVM之堆(Heap)
  • 【网络编程】TCP 服务器并发编程:多进程、线程池与守护进程实践
  • 智能体赋能金融多模态报告自动化生成:技术原理与实现流程全解析
  • 数据库(一)数据库基础及MySql 5.7+的编译安装
  • 将 x 减到 0 的最小操作数
  • Java 开发工具,最新2025 IDEA使用(附详细教程)
  • 基于STM32单片机的OneNet物联网粉尘烟雾检测系统
  • 注意力机制与常见变种-概述
  • Linux内核TCP协议实现深度解析
  • 数据治理进阶——40页数据治理的基本概念【附全文阅读】
  • Spring Boot 与前端文件下载问题:大文件、断点续传与安全校验
  • 认知语义学中的象似性对人工智能自然语言处理深层语义分析的影响与启示
  • 游戏服务器使用actor模型
  • 002 Rust环境搭建
  • 2.11组件之间的通信---插槽篇
  • 关于java中的String类详解
  • S3C2440 ——UART和I2C对比
  • TDengine 数据写入详细用户手册