并发编程四、相关锁的概念总结

前言:

  1. 文章内容:线程与进程、线程生命周期、线程中断、线程常见问题总结
  2. 本文章内容来源于笔者学习笔记,内容可能与相关书籍内容重合
  3. 偏向于知识核心总结,非零基础学习文章,可用于知识的体系建立,核心内容复习,如有帮助,十分荣幸
  4. 相关文献:并发编程实战、计算机原理

 悲观锁、乐观锁


悲观锁:

  对于同一个数据的并发操作,悲观锁认为我在修改数据时一定会有其他线程进来修改数据,因此在操作数据之前进行加锁。这种思路实现的锁就是悲观锁:java中synchronized和ReentrantLock都是悲观锁。

乐观锁:

  对于同一个数据的并发操作,乐观锁认为我在修改数据时不会有其他线程进来修改数据,因此不会添加锁来限制其他线程的进入,只有在更新数据时,判读之前有没有线程更新了这个数据,如果这个数据没有被更新,那么当前线程就有更新的权利,如果被更新了,报错或重试。
  举个栗子:git提交代码会检查远端版本是不是领先于我们现在,如果不一致就会提示其他修改了远端代码,提交失败。如果版本号一致,就可以提交。

悲观锁缺点:

  1. 阻塞和唤醒带来性能优势,线程切换消耗
  2. 永久阻塞:如果持有锁的线程被永久阻塞,比如无限循环,死锁等活跃性问题。那么等待该线程释放锁的其他线程,永远得不到执行
  3. 优先级反转:线程有优先级,可能当优先级低的线程拿到锁,执行很久释放慢。可能导致优先级高的一直拿不到锁,优先级错乱

乐观锁缺点:

  悲观锁原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响。相反,乐观锁一开始开销小,但是自旋时间很长或不停重试,那么消耗的资源也会越来越多

结论:

  • 悲观锁适合写操作较多的场景,加锁保证写操作时数据正确。或者临界区持锁时间比较长的情况,悲观锁可以避免大量无用自旋消耗。比如:临界区有IO操作、临界区代码复杂、临界区竞争激烈。
  • 乐观锁适合读操作较多,并发写少的场景,不加锁能够使其读操作性能大幅提升
  • 乐观锁也称无锁编程,典型实现就是CAS算法

公平锁、非公平锁


公平锁:

  获取锁的顺序符合请求的绝对时间顺序,没有获取到锁的线程会被安排到一个阻塞队列中去,也就是FIFO,缺点是吞吐量很低,因为除了等待队列中的第一个线程,其他的线程全部会被阻塞,而 CPU唤醒阻塞线程也是需要很多开销的。

非公平锁:

  1. 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
  2. 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

两个锁的优劣:

  • 优势:公平锁各线程平等,每个线程等待一段时间后,总有执行机会。非公平锁更快,吞吐量更大
  • 劣势:公平锁更慢,吞吐量更新。非公平锁可能产生线程饥饿

死锁


   两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

发生条件:缺一不可

  1. 互斥:一个资源每次只能被一个线程使用
  2. 请求与保持:线程持有已经分配给他们的资源,同时等待其他的资源
  3. 不抢占:线程已经获取到的资源不会被其他线程强制抢占;
  4. 环路等待:线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。

如何定位死锁:

  jstack或jconsole获取线程栈,如果比较明显的死锁关系,工具可以直接检测。如果不明显,就可以分析线程栈看看相互依赖关系。线程栈包含了线程加锁信息,比如哪个线程获得了哪个锁,在哪个语句中获取的,以及正在等待或阻塞的线程是哪个等信息。

如何避免:

  • 破坏环路等待:对加锁操作进行排序,让要申请资源的线程单向等待锁释放,无法形成环路
  • 破坏请求与保持:一次性原子的获取所有需要的锁,比如通过全局锁作为加锁令牌控制加锁操作,只有获取了这个令牌才能执行加锁操作。这种方式会降低系统并发性,所有需要获取锁的线程都要去竞争同一个加锁令牌所。并且一开始就获取了所有的锁,导致线程持有锁的时间超出了实际需要,很多资源被长时间持有浪费。
  • 破坏不抢占:让线程在获取后续的锁失败时,主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行。但是这样也会造成几个互相竞争的线程不断放弃、重试可能导致活锁。可以再重试操作加一个随机延迟时间,降低任务冲突的概率
  • 破坏互斥:使用CAS或并发类,来避免互斥锁带来的开销和复杂性

活锁:

  1. 线程并没有阻塞,始终在运行,但是程序并没有进展,因为线程再做重复的事情。而死锁是线程阻塞了都在相互等待资源释放。
  2. 活锁的解决方式就是,线程的重试机制要变化,引入随机因素。不能让线程重试都是一样的机制,那么就是反复在执行相同的逻

防止死锁:

  • 尽量使用 tryLock去获取锁,并设置超时时间,超时可以退出防止死锁。
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
  • 尽量减少同步的代码块。
 

 

 

 

 

 

 

 

 


 

posted @ 2022-08-30 10:39  难得  阅读(31)  评论(0编辑  收藏  举报