并发编程四、相关锁的概念总结
前言:
- 文章内容:线程与进程、线程生命周期、线程中断、线程常见问题总结
- 本文章内容来源于笔者学习笔记,内容可能与相关书籍内容重合
- 偏向于知识核心总结,非零基础学习文章,可用于知识的体系建立,核心内容复习,如有帮助,十分荣幸
- 相关文献:并发编程实战、计算机原理
悲观锁、乐观锁
悲观锁:
对于同一个数据的并发操作,悲观锁认为我在修改数据时一定会有其他线程进来修改数据,因此在操作数据之前进行加锁。这种思路实现的锁就是悲观锁:java中synchronized和ReentrantLock都是悲观锁。
乐观锁:
对于同一个数据的并发操作,乐观锁认为我在修改数据时不会有其他线程进来修改数据,因此不会添加锁来限制其他线程的进入,只有在更新数据时,判读之前有没有线程更新了这个数据,如果这个数据没有被更新,那么当前线程就有更新的权利,如果被更新了,报错或重试。
举个栗子:git提交代码会检查远端版本是不是领先于我们现在,如果不一致就会提示其他修改了远端代码,提交失败。如果版本号一致,就可以提交。
悲观锁缺点:
- 阻塞和唤醒带来性能优势,线程切换消耗
- 永久阻塞:如果持有锁的线程被永久阻塞,比如无限循环,死锁等活跃性问题。那么等待该线程释放锁的其他线程,永远得不到执行
- 优先级反转:线程有优先级,可能当优先级低的线程拿到锁,执行很久释放慢。可能导致优先级高的一直拿不到锁,优先级错乱
乐观锁缺点:
悲观锁原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响。相反,乐观锁一开始开销小,但是自旋时间很长或不停重试,那么消耗的资源也会越来越多
结论:
- 悲观锁适合写操作较多的场景,加锁保证写操作时数据正确。或者临界区持锁时间比较长的情况,悲观锁可以避免大量无用自旋消耗。比如:临界区有IO操作、临界区代码复杂、临界区竞争激烈。
- 乐观锁适合读操作较多,并发写少的场景,不加锁能够使其读操作性能大幅提升
- 乐观锁也称无锁编程,典型实现就是CAS算法
公平锁、非公平锁
公平锁:
获取锁的顺序符合请求的绝对时间顺序,没有获取到锁的线程会被安排到一个阻塞队列中去,也就是FIFO,缺点是吞吐量很低,因为除了等待队列中的第一个线程,其他的线程全部会被阻塞,而 CPU唤醒阻塞线程也是需要很多开销的。
非公平锁:
- 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
- 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
两个锁的优劣:
- 优势:公平锁各线程平等,每个线程等待一段时间后,总有执行机会。非公平锁更快,吞吐量更大
- 劣势:公平锁更慢,吞吐量更新。非公平锁可能产生线程饥饿
死锁
两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
发生条件:缺一不可
- 互斥:一个资源每次只能被一个线程使用
- 请求与保持:线程持有已经分配给他们的资源,同时等待其他的资源
- 不抢占:线程已经获取到的资源不会被其他线程强制抢占;
- 环路等待:线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。
如何定位死锁:
jstack或jconsole获取线程栈,如果比较明显的死锁关系,工具可以直接检测。如果不明显,就可以分析线程栈看看相互依赖关系。线程栈包含了线程加锁信息,比如哪个线程获得了哪个锁,在哪个语句中获取的,以及正在等待或阻塞的线程是哪个等信息。
如何避免:
- 破坏环路等待:对加锁操作进行排序,让要申请资源的线程单向等待锁释放,无法形成环路
- 破坏请求与保持:一次性原子的获取所有需要的锁,比如通过全局锁作为加锁令牌控制加锁操作,只有获取了这个令牌才能执行加锁操作。这种方式会降低系统并发性,所有需要获取锁的线程都要去竞争同一个加锁令牌所。并且一开始就获取了所有的锁,导致线程持有锁的时间超出了实际需要,很多资源被长时间持有浪费。
- 破坏不抢占:让线程在获取后续的锁失败时,主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行。但是这样也会造成几个互相竞争的线程不断放弃、重试可能导致活锁。可以再重试操作加一个随机延迟时间,降低任务冲突的概率
- 破坏互斥:使用CAS或并发类,来避免互斥锁带来的开销和复杂性
活锁:
- 线程并没有阻塞,始终在运行,但是程序并没有进展,因为线程再做重复的事情。而死锁是线程阻塞了都在相互等待资源释放。
- 活锁的解决方式就是,线程的重试机制要变化,引入随机因素。不能让线程重试都是一样的机制,那么就是反复在执行相同的逻
防止死锁:
- 尽量使用 tryLock去获取锁,并设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
本文来自博客园,作者:难得,转载请注明原文链接:https://www.cnblogs.com/zhangbLearn/p/16638459.html
分类:
并发编程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2018-08-30 Rabbitmq-topic演示