JavaEE初阶5.0
之前的多线程1.0--4.0是初阶部分 现在开始进入多线程进阶部分(八股)
目录
一、常见的锁策略
1.0 悲观锁和乐观锁
2.0 重量级锁和清量级锁
3.0 挂起等待锁和自旋锁
4.0 普通互斥锁和读写锁
5.0 可重入锁和不可重入锁
6.0 公平锁和不公平锁
二、sychronized详细情况
1.0 锁升级
2.0 锁消除
3.0 锁粗化
4.0 CAS
(1)概念
(2)两种典型用途
(3)ABA问题及解决方案
三、JUC
1.0 Callable接口
2.0 ReentrantLock
3.0 Semaphore
4.0 CountDownLatch
5.0多线程下使用ArrayList
一、常见的锁策略
为什么要引入锁策略
不同场景需求不同 于是Java设计了不同的锁策略 用来应对不同的场景
在保证线程安全的前提下,提高效率,避免死锁,适应不同场景
那么 有哪些锁策略?
1.0 悲观锁和乐观锁
不是针对某一种具体的锁 而是某个锁具有悲观特性或者乐观特性
悲观:加锁的时候 预测接下来的锁竞争的情况非常激烈 就需要针对这样激烈情况额外做一些情况
通俗:你一上厕所,一进去就把门锁死,默认外面的人会来抢坑位
场景如 银行转账
乐观:加锁的时候,预测接下来的锁竞争的情况不激烈,就不需要额外的工作
通俗:上厕所不锁门,但如果发现有人进来 就退出去重试(共同填写表格 出现冲突的时候才解决
而不是排队填表格)但是会产生无锁或者自旋(忙等)
场景如:读多写少 点赞数 文章阅读量
总结:悲观 保证同一时间只有一个线程能进入某方法 其他线程必须等待锁释放才能执行
乐观:先操作 提交时检查是否冲突
区别:核心思想不同 操作前加锁 防止别人干扰/先操作,提交时检查冲突
并发性能不同 阻塞等待 无锁 但冲突多的时候自旋消耗CPU
适用于写多读少/读多写少
各有优缺点 要根据合适的场景分别使用
2.0 重量级锁和清量级锁
重量级锁 当悲观的场景下 此时就要付出更多的代价
轻量级锁 应对乐观场景下 此时付出的代价更小
通俗:重量级 像是生活中银行办理业务的排队 只有一个柜台,所有人必须严格排队(线程阻塞)
轻量级 像是超市自主收银 顾客自己扫码支付
轻量级锁的缺点是自旋浪费cpu资源 此时会升级为重量级锁
3.0 挂起等待锁和自旋锁
挂起等待锁 操作系统内核级别的 加锁的时候发现竞争 就会使该线程进入阻塞状态 后续就需要内 核唤醒了
自旋锁 应用程序级别的 加锁的时候 一般也不是进入阻塞 而是通过忙等的形式来等待
不涉及内核操作 加锁失败后等待 不会放弃cpu 14:01问问 14:02问问 14:03问问
synchronized是悲观还是乐观呢?
jvm内部会统计每个锁的激烈程度 然后自己做出调整(jvm的大佬为咱们小卡拉米操碎了心~)
4.0 普通互斥锁和读写锁
普通互斥锁:简单粗暴,适合 写多读少,但读操作也阻塞。 synchronized
读写锁:精细控制,适合 读多写少,读操作可并发,写操作独占。
通俗:普通互斥锁还是相当于厕所一次只能进一个人 无论读(上厕所) 还是写(打扫厕所)
读写锁相当于图书馆允许多个人同时看书 但是当有人修改书的内容的时候,禁止其他人读
或者写。
各有优缺点,要看具体使用场景
5.0 可重入锁和不可重入锁
之前讲过 synchronized是可重入锁
要点 锁要记录当前是哪个线程拿到这把锁的
使用计数器记录当前加锁了多少次 在合适的时候进行解锁
6.0 公平锁和不公平锁
公平的定义是什么? 是先来后到还是概率优先? (银行排队业务还是地铁抢座位呢)
Java中的公平锁的定义是严格按照线程请求顺序(先来后到)
synchronized是非公平锁
公平锁也需要额外的代价:例如 使用一个队列记录一下各个线程获取锁的顺序
一般就是问个概念。面试官问到某个问题的时候 用到上述术语
二、sychronized详细情况
sychronized是Java大佬精心设计的一把锁 能自适应(上面讲的部分)
1.0 锁升级
无锁-->偏向锁--->自旋锁-->重量级锁
偏向锁:进行synchronized 刚一上来 不是真的加锁 而是只是简单做一个标记(搞暧昧)这个标记非常轻量 相比于加锁解锁来说 效率高很多~
如果没有其他线程来竞争这个锁 最终当前线程执行到解锁代码 也就是只是简单清楚上述标记即可(不涉及真加锁 真解锁) (搞暧昧 不真确立关系 后续分手就很快)
如果有写其他线程来竞争 就抢先一步, 在另一个线程拿到锁之前 抢先拿到锁 真加锁了
偏向锁就升级为轻量级锁 其他线程只能阻塞等待
2.0 锁消除
也是编译器优化代码的一种体现
编译器会判定 当前这个代码是否真的需要加锁
如果确实不需要加锁 但是你写了synchronized 就会自动把synchronized给去掉
3.0 锁粗化
锁的粒度:加锁和解锁之间 包含的代码越少 就认为锁的颗粒度越粗
如果包含的代码越少 就认为锁的粒度越细
核心思想:如果频繁地加锁解锁太麻烦,不如直接锁住一大段代码
4.0 CAS
(1)概念
多线程中一个很膈应的东西 一般实际开发中很少直接使用CAS(尤其是Java)但是确实很多地方都有它的影子~
CAS compare and swap 比较和交换
咱们谈到的CAS 是CPU的一条原子指令
核心功能是:比较内存中的值是否等于预期值 如果是 则修改为新值 否则不做任何操作
CAS是并发编程的“无锁秘籍” 但需警惕ABA和自旋
还记得学习多线程安全引入的第一个例子吗 count加到10万那个
前面典型的操作 count++ 线程不安全 就需要加锁来解决问题
但是又认为 加锁效率比较低 于是就可以通过CAS来实现count++ 确保性能,同时也保证线程安全
(使用原子类的目的 就是为了避免加锁) (原子类 专有名词 特指atomic这个包里的类)
原子类:原子类是 Java 提供的线程安全、无锁的工具类,基于 CAS 实现,用于高效解决多线程环境下的共享变量原子操作问题。
(2)两种典型用途
原子类保证线程安全
这个过程演示了 CAS保证线程安全的原理
即使上述代码中出现线程切换 由于在自增之前,先判定当前寄存器读到的值是否是“科学的值”
如果是不科学的值 就会重新读取~
还可以基于CAS实现自旋锁
(3)ABA问题及解决方案
CAS的一个典型缺陷: 使用CAS能够进行线程安全的编程 核心就是先比较“相等”(内存和寄存器是否相等)(这里本质上是在判定是否有其他线程插入进来了做了一些修改如果发现这里寄存器和内存的值一致,就可以认为没有线程穿插过来修改 因此接下来的修改操作就是线程安全的) 但是如果原来是A 中途有线程A-B-A(翻新机)呢另一个线程把内存A修改成B 又从B修改回A呢)
CAS中的ABA问题 其实大部分情况下 即使出现了ABA 最终的程序,一般问题也不大
只有在一些极端的场景 ABA问题才会产生一些严重的bug
例子1:
余额1000 取500 点下取款的时候 卡了一下 紧接着 我咔咔狂按了好几下取款(电梯)
此时就可能出现两个线程 并发的执行扣款操作~
这样看确实没什么问题
但是赶巧了 这个时候有另一个线程给账户转账500 这样看起来还是1000(ABA)
(另一个线程给账户转账1000就好了 不会出现这个问题哈哈)
虽然这样是小概率问题 但是乘以一个很大的基数 也会变成大问题
上述问题中 使用钱(余额)数值来判定中间是否有线程穿插修改 余额 可加可减
如果换成其他的指标 约定 只能加不能减 有效的避免ABA问题
版本号(CAS不仅要比值 还要比较版本号)这种思想相当于是原子性里面再套一层原子
时间(闰秒问题)
三、JUC
juc中的一些组件 java.util.concurrent 就是一些和多线程相关的工具~
1.0 Callable接口
Callable接口和Runnable接口并列关系
Callable接口 返回值是call() 泛型参数
Ruannable void run()
Thread的构造方法 没有提供版本 传入callable对象
这个futureTask相当于去饭店吃饭的时候领的一个号码牌 凭号码牌取餐
2.0 ReentrantLock
reentrant 可重入 它和 synchronized是并列的关系
例子:
reentrack可以理解为上古时期的写法 后来的synchronized明显更香~
synchronized和ReentrantLock之间的区别:
(经典面试题 这篇文章主要就是为了应付面试的内容)
3.0 Semaphore
信号量 :主要最为进程之间以及同一进程的不同线程之间的同步和互斥手段
可以理解为一个计数器 描述了某种可用资源的个数
申请一个资源 计数器就会+1 释放一个资源 计数器就会-1
计数器为0 继续申请 就会阻塞等待
4.0 CountDownLatch
使用多线程 经常把一个大的任务拆分成多个子任务
使用多线程执行这些任务 从而提高程序的效率
如何衡量 这多个子任务都完成了呢?
结合具体代码 感受一下具体的使用情况
5.0多线程下使用ArrayList
ArrayList在多线程是不安全的 如何安全的使用呢?
(1)自行加锁 (最推荐的方式) 具体问题具体分析
(2)Collections.synchronizedList(new ArrayList);
这个相当于是套壳 返回的List的各种关键的方法都是带有synchronized的
相当于粗暴的都加上锁了 但是这样做也是有代价的 作为一种了解就行
(3)使用CopyOnWriteArrayList
重点是介绍一下CopyOnWrite 编程中一种常见思想方法 写时拷贝
多线程修改/读取不同变量 不会有问题
一旦某个线程进行写操作 比如修改1->100 复制的过程中,如果其他线程在读 就直接读取旧的版本数据 虽然复制的过程不是原子的(也消耗一定的时间)由于提供了旧版本数据,不影响其他线程读取 新版本数组复制完毕之后,直接进行引用的修改 引用的赋值是“原子”(确保不会读到“修改一半的数据~)
这个过程没有加锁 也就不会产生阻塞 但是也有明显的缺点
数组特别大 必然非常低效(拷贝很大的数组)
如果多个线程同时修改 也容易出现问题(复制多份 最终这个引用指给谁呢)
所以比较适合于一些特定的场景方案(服务器进行重新加载配置的时候~)
//6.0 多线程使用哈希表004914
感谢大家的支持
更多内容还在加载中...........
如有问题欢迎批评指正,祝大家生活愉快、学习顺利!!!