2023.10.27 常见的 锁策略 详解

目录

相关专业名词

信号量 Semaphore

互斥锁 和 读写锁

乐观锁 和 悲观锁

轻量级锁 和 重量级锁

自旋锁 和 挂起等待锁 

公平锁 和 非公平锁

可重入锁 和 不可重入锁


相关专业名词

上下文切换

  • 上下问切换指的是将当前执行的线程或进程的上下文保存卡来,然后切换到另一个线程或进程的上下文,使得它可以继续执行
  • 当切换回原来的线程或进程时,之前保存的上下文将被恢复,使得程序可以从切换前的状态继续执行

临界区

  • 指多线程或多进程环境下访问共享资源的一段代码区域
  • 临界区的目的时确保同时只有一个线程或进程可以进入该区域,并且对共享资源的访问是互斥的

信号量 Semaphore

  • 是一种用于控制多个线程对共享资源访问的同步机制

基本思路

  • 当线程需要访问共享资源时,首先尝试获取信号量
  • 如果信号量的计数器大于零,表示资源可用,线程可以继续执行并将计数器 -1
  • 如果信号量的计数器等于零,表示资源已经被占用,线程需要等待,加入到等待队列中
  • 当资源被释放时,信号量的计数器 +1,并从等待队列中选择一个线程唤醒,使其继续执行

主要操作

  • 初始化:设置信号量的初始计数器值
Semaphore semaphore = new Semaphore() //括号中填入初始资源数(阈值)
  • P 操作(也称为 wait 操作):尝试获取信号量,如果计数器大于零则 -1,如果等于零,则线程阻塞等待
  • V 操作(也称为 signal 操作):释放信号量,将计数器 +1,并唤醒一个等待的线程

使用场景

  • 可以用于解决多线程环境下的资源互斥访问和线程同步问题
  • 通过适当设置信号量的计数器,可以控制对共享资源的并发访问数量,实现资源的有序访问和互斥访问

注意

  • 除了常用的二进制信号量,即只有 0 和 1 两种状态的信号量
  • 还有计数信号量,它的计数器可以是任意正整数,允许多个线程同时访问资源

总结

  • 信号量本身并不解决竞争条件和死锁问题,而是提供了一种机制来协调和控制线程对共享资源的访问

互斥锁 和 读写锁

互斥锁

  • 互斥锁是一种独占锁机制

基本思路

  • 它确保在任何时刻只有一个线程可以获得锁,其他线程需要等待锁的释放
  • 当一个线程获得互斥锁后,它可以访问共享资源并执行相应的操作,其他线程则被阻塞,直到锁被释放

优点

  • 确保了共享资源的独占访问问,避免了数据竞争和不一致的问题

适用场景

  • 共享资源需要被互斥访问,即同一个时间只能有一个线程访问
  • 锁竞争激烈的情况下,只有一个线程可以获得锁

读写锁

  • 读写锁是一种共享锁机制

基本思路

  • 它允许多个线程同时对共享资源进行读操作,但在进行写操作时需要互斥访问

特点

  • 多个线程可以同时获取读锁,实现共享读取
  • 写锁是独占的,当一个线程持有写锁时,其他线程无法获取读锁或写锁

优点

  • 读写锁在读操作较多、写操作较少的场景下可以提供更高的并发性
  • 多个线程可以同时进行读操作,提高了系统的并发能力

适用情况

  • 共享资源的读操作远远多于写操作
  • 写操作对共享资源的修改需要互斥访问,以确保数据的一致性

乐观锁 和 悲观锁

  • 站在锁冲突概率的预测角度

乐观锁

  • 它认为在大多数情况下,并发访问不会发生冲突

基本机制

  • 当使用乐观锁时,一般会采用 版本号 或 时间戳 等机制来检查并发冲突
  • 在更新资源共享之前,会先读取当前的 版本号 或 时间戳,并在更新时再次检查是否发生了变化
  • 如果发生了变化,说明其他线程或用户已经修改了资源,当前操作可能会导致数据不一致
  • 因此需要重写读取最新的资源并重新尝试更新操作

缺点

  • 乐观锁在处理并发冲突时,通常不会阻塞其他线程或用户的访问,而是允许并发操作,并在冲突发生时进行回滚或重试
  • 这增加了系统设计和实现的复杂性,需要考虑并发操作的正确性、一致性、异常处理等方面

总结

  • 乐观锁就是指锁冲突的概率不高,因此做的工作就可以简单一些,因此性能也比较高,但往往不能处理到所有问题,需要一定的系统复杂度来应对这些情形

悲观锁

  • 它认为在并发环境下,会发生竞争和冲突

基本机制

  • 悲观锁的经典实现是使用 互斥锁 或 信号量
  • 在使用悲观锁的默认情况下,当一个线程或用户要访问共享资源时,他会先获取锁,阻止其他线程或用户对资源进行修改,直到自己完成操作后才会释放锁
  • 这样可以确保同一时间只有一个线程或用户可以访问共享资源,从而避免了并发冲突

总结

  • 悲观锁会阻塞其他线程或用户的访问,以确保数据的安全性,但性能相对较低
  • synchronized 关键字就是一个典型的悲观锁机制

轻量级锁 和 重量级锁

  • 站在加锁开销的角度

轻量级锁

  • 加锁机制尽可能不通过系统调度来进行用户态和内核态的切换,而是尽量在用户态完成,实在不行再切换,即典型的纯用户态加锁逻辑,开销较小

核心思想

  • 在没有锁竞争时,使用 CAS 操作将对象头部的一部分标记为锁标记(锁记录),而不是直接使用 互斥锁 来实现
  • 当线程尝试获取轻量级锁时,它会使用 CAS 操作尝试将锁标记变成自己的线程ID
  • 如果 CAS 操作成功,表示该线程成功获取到锁,可继续执行临界区代码
  • 如果 CAS 操作失败,表示有其他线程竞争锁,此时会升级为重量级锁 

优点

  • 在无竞争的情况下性能较好
  • 因为它避免了线程阻塞和唤醒的开销,减少了上下文切换的代价

缺点

  • 在有竞争的情况下,会频繁进行 CAS 操作,如果 CAS 操作一直失败,会升级为重量级锁,导致性能下降

总结

  • 轻量级锁适用于竞争不激烈的情况,在无竞争的情况下性能较好

重量级锁

  • 重量级锁是传统的线程同步机制,通常使用 互斥量 或 信号量 实现

核心思想

  • 当一个线程获取重量级锁时,如果锁已经被其他线程占用,该线程会被阻塞,进入睡眠状态,直到获取到锁的线程释放锁并唤醒等待线程

优点

  • 在有竞争的情况下可以确保线程的正确同步,不会出现数据竞争的问题

缺点

  • 在线程切换和阻塞唤醒的过程中存在比较大的开销,包括切换上下文、线程调度、内核态于用户态切换等,会降低系统的性能

总结

  • 重量级锁适用于竞争激烈的情况,能确保线程正确同步,但性能相对较低

自旋锁 和 挂起等待锁 

  • 站在线程 加锁快慢 的角度 


自旋锁

  • 是一种典型的轻量级锁实现方式

基本思路

  • 自旋锁是一个 忙等 的锁机制
  • 线程尝试在回去锁时,不会立即进入睡眠状态,而是通过循环不断地检查锁的状态,直到获取到锁为止
  • 自旋锁通常使用原子操作(CAS 操作)来实现

优点

  • 避免了线程切换和上下文切换的开销,因为线程不会进入睡眠状态

缺点

  • 长时间的自旋会占用CPU资源,造成性能损失
  • 当线程竞争激烈或持有锁时间比较长时,自旋的效率会下降

适用场景

  • 线程持有锁的时间较短,不会导致其他线程长时间等待
  • 线程的竞争不激烈,获取锁的时间短

挂起等待锁

  • 是一种典型的重量级锁实现方式

基本思路

  • 挂起等待所是一种线程阻塞的锁机制
  • 线程在尝试获取锁时,如果锁已经被其他线程占用,该线程会进入睡眠状态,释放 CPU 资源,直到锁被释放并且被唤醒后再重新尝试获取锁

优点

  • 可以有效避免自旋锁的性能问题,因为线程再等待锁时会释放 CPU 资源,不会占用过多的 CPU 时间

缺点

  • 线程的阻塞和唤醒需要操作系统的支持,会引入额外的开销
  • 线程的阻塞和唤醒可能会引起上下文切换,影响系统的性能

适用场景

  • 线程竞争激烈,可能会有较长的等待时间
  • 线程持有锁时间较长,不希望占用 CPU 资源

公平锁 和 非公平锁

  • 站在线程 获取锁 的角度

公平锁

  • 指多个线程在竞争锁时,按照它们发出请求的顺序来获取锁

基本思路

  • 当一个线程请求获取锁时,如果锁是可用的,该线程会直接获取锁
  • 如果锁已经被其他线程占用,该锁会进入等待队列,等待锁的释放
  • 当锁被释放时,等待时间最长的线程 会被唤醒并获取锁

优点

  • 确保了锁的获取按照请求的顺序进行,避免了线程饥饿现象
  • 所有线程都有公平竞争的机会

适用场景

  • 对线程的公平性有较高的要求,希望避免线程饥饿现象

非公平锁

  • 指多个线程在竞争锁时,不考虑它们发出请求的顺序,直接尝试获取锁

基本思路

  • 当一个线程请求获取锁时,如果线程时可用的,该线程会直接获取锁
  • 如果锁已经被其他线程占用,该线程会进入竞争,与其他线程一起竞争锁的所有权

缺点

  • 可能会导致某些线程长时间等待,产生线程饥饿现象

适用场景

  • 追求更高的系统吞吐量,并且对线程的公平性要求不高

总结

  • 操作系统和 synchronized 原生都是 "非公平锁"
  • 操作系统对这里的针对加锁的控制,本身就依赖系统调度顺序的
  • 这个调度顺序是随机的,不会考虑到这个线程等待锁多久了
  • 要想实现公平锁,就得在这个基础上,引入一个队列,让这些想加锁的线程去排队

可重入锁 和 不可重入锁

  • 站在 线程是否能重复获取同一个锁的 角度

可重入锁

  • 指同一个线程可以多次获取同一个锁而不会造成死锁

基本思路

  • 当线程第一次获取锁后,锁会记录该线程的持有者和持有计数
  • 在该线程持有锁的期间,它可以再次获取锁而不会被阻塞,而是增加持有计数
  • 当线程释放锁时,持有计数递减,直到计数为零时锁完全被释放

优点

  • 方便了对共享资源的嵌套访问
  • 如果一个线程已经获取了某个锁,那么在持有这个锁的期间,它可以安全的调用其他需要获取通过一个锁的代码

不可重入锁

  • 指同一个线程在持有锁的情况下,再次获取锁时会被阻塞

基本思路

  • 不可重入锁的一个典型例子是简单的互斥锁,它只允许一个线程在任意时刻获取锁
  • 如果一个线程已经持有锁,再次请求获取锁时会被阻塞,直到锁被释放

缺点

  • 使用不够方便,在编写复杂的嵌套代码结构时可能会导致死锁和其他问题

总结

  • 可重入锁是更常见和推荐的选择

你可能感兴趣的:(多线程,java,数据库,开发语言)