JavaEE 初阶 -- 多线程进阶

常见锁策略

乐观锁 VS 悲观锁

锁的实现者,预测接下来锁冲突(就是锁竞争,两个线程针对一个对象加锁,产生阻塞等待了)的概率是大,还是不大,来决定接下来咋做。

乐观锁:预测接下来冲突概率不大
悲观锁:预测接下来冲突概率比较大
通常来说,悲观锁一般要做的工作更多一些,效率会更低一些,乐观锁做的工作会更少一点,效率更高一点(但是并不绝对)

轻量级锁 VS 重量级锁

轻量级锁,加锁解锁,过程更快更高效
重量级锁,加锁解锁,过程更慢更低效

一个乐观锁可能也是一个轻量级锁,一个悲观锁可能也是一个重量级锁(都不绝对)。
他们和乐观悲观锁,虽然不是一回事,但确实有一定的重合。

自旋锁 VS 挂起等待锁

自旋锁是轻量级锁的一种典型实现
挂起等待锁是重量级锁的一种典型实现

自旋锁通常是纯用户态的,不需要经过内核态(时间相对更短)
挂起等待锁通过内核的机制来实现挂起等待(时间更长了)

自旋锁,一旦锁被释放,就能第一时间拿到锁,速度会更快,不过会忙等,消耗CPU资源。
挂起等待锁,如果锁被释放,不能第一时间拿到锁,可能需要过很长时间才能拿到锁,但是这个空出来的时间,是可以干别的事情的。

  • 针对上面3种策略,synchronized这把锁属于哪种?
    在这里插入图片描述

互斥锁 VS 读写锁

synchronized,是互斥锁,加锁,就只是单纯的加锁,没有更细化的区分了,像它这样的锁只有两个操作:1. 进入代码块,加锁 2. 出了代码块,解锁

除了这种之外,还有一种读写锁,能够把读和写,两种加锁区分开
读写锁:1. 给读加锁 2. 给写加锁 3.解锁

读写锁中约定:

  1. 读锁和读锁之间,不会锁竞争,不会产生阻塞等待,就不会影响程序的速度,代码还是跑的很快
  2. 写锁和写锁之间,有锁竞争
  3. 写锁和读锁之间,也有锁竞争
    2和3减慢了速度,但是保证准确性,所以读写锁更加适合于,一些多读的情况

标准库中提供了另外两个专门的读写锁,读锁是一个类,写锁是一个类
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.

可重入锁 VS 不可重入锁

如果一个锁,在一个线程中,连续对该锁加锁两次,不死锁,就叫做可重入锁,如果死锁了,就叫不可重入锁

在这里插入图片描述
可是实际上,在java中并不会死锁,因为synchronized是一个可重入锁,在加锁的时候,会判定一下,看当前尝试申请锁的线程是不是已经就是锁的拥有者了,如果是,就直接放行!

关于死锁的情况

  1. 一个线程,一把锁,就是上面这种情况,可重入锁没事,不可重入锁死锁
  2. 两个线程,两把锁,即使是可重入锁,也会死锁
  3. N 个线程,M 把锁

在这里插入图片描述

在这里插入图片描述

死锁的4个必要条件(缺一不可)
  1. 互斥使用:一个线程拿到一把锁之后,另外一个线程不能使用
  2. 不可抢占:一个线程拿到锁,只能自己主动释放,不能被其它线程强行占有
  3. 请求和保持:当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的(”吃着碗里的,惦记着锅里的“)
  4. 循环等待:等待形成了一个环(哲学家吃面)

公平锁和非公平锁

约定,遵守先来后到的是公平锁,不遵守的是非公平锁。
操作系统对于线程的调度是随机的。自带的synchronized这个锁,是非公平的,想要实现非公平锁,需要在synchronized的基础上,加上一个队列来记录这些加锁线程的顺序。

学到这里,我们来总结一下synchronized:

synchronized的特点

  1. 既是乐观锁,也是悲观锁
  2. 既是轻量级锁,也是重量级锁
  3. 轻量级锁基于自旋实现,重量级锁基于挂起等待实现
  4. 不是读写锁,是互斥锁
  5. 是可重入锁
  6. 是非公平锁

关于锁策略的几个面试题

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

2. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中

3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁:
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

4. synchronized 是可重入锁么?

是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增

CAS

什么是CAS

CAS :compare and swap
它做的事情:拿着寄存器 A 的值和内存 M 的值进行比对,如果值相同,就把寄存器 B 的值和 M 的值进行交换。
这个寄存器 B,大概率也是来源于另一个内存,而且更多时候,不关心寄存器中的值,更关心内存是数值(就是变量的值)。

在这里插入图片描述

基于CAS实现的操作

1. 实现原子类

标准库里提供了一组原子类。
针对锁常用的一些,int,long,数组等等进行了封装(比如对于整形的就是AtomInteger),可以基于CAS的方法进行修改,并且线程安全。

在这里插入图片描述
这些操作,要比之前加锁操作,执行的更快,synchronized会涉及到锁的竞争,两个线程要相互等待,有了等待过程,代码执行的速度就会慢下来,但是CAS实现的 ++ 并不会涉及到线程的阻塞等待,也就是说,两个线程同时进行非常的快!

实现自旋锁

在这里插入图片描述

  • 有人问,CAS保证内存可见性吗?

不能!只能保证原子性,无法保证内存可见性,而且对于原子类 AtomicInteger 源码中的 value 也是加了 volatile 修饰的。举个例子:假设有两个线程 T1 和 T2 同时对变量 X 进行修改。初始状态下,变量 X 的值为0。T1将 X 的值增加了1,T2将 X 的值增加了2。如果 T1 和 T2 都使用了 CAS 操作,则最终的变量 X 的值应该为3。然而,在多核处理器的架构下,由于每个线程都有自己的高速缓存,当 T1 和 T2 分别对 X 进行 CAS 操作时,它们可能只会将自己的缓存中的 X 的值更新一次,而不会直接写回到主存中。具体来说,如果 T1 和 T2 在不同的核心上执行,那么它们寄存器中的 X 值会被写入各自核心的高速缓存中,并不会立即写回到主存。因此,当 T2 要修改 X 的时候,它并不会获取到

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值