多线程之常见的锁策略

简介: 多线程之常见的锁策略

一、乐观锁 VS 悲观锁

站在锁冲突概率的角度来看,冲突较少,锁竞争不激烈,此时为乐观锁,反之,锁竞争很激烈则升级为悲观锁。

对于乐观锁来说,假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何做。

对于悲观锁来说,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适。

synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略。

乐观锁的一个重要功能就是要检测出数据是否发生访问冲突,我们可以引入一个 "版本号" 来解决,只要访问一次,版本号就会增加一次,所以版本号是唯一的,可以用来校验。


二、互斥锁 VS 读写锁


关于互斥锁,就是像synchronized这样的锁,提供加锁和解锁两个操作。如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。

关于读写锁,提供了三种操作:针对读加锁、针对写加锁、解锁。因为多线程针对同一个变量进行并发读,这个时候没有线程安全问题的,也不需要加锁控制。读锁和读锁之间,没有互斥;写锁和写锁之间,存在互斥;写锁和读锁之间,存在互斥。

在代码中,如果只是读操作,加读锁即可,如果有写操作,加写锁。

假设如果当前有一组线程都去读(加读锁),这些线程之间没有锁竞争的,也没有线程安全问题。

假设如果当前的一组操作有读也有写,才会产生锁竞争。

在很多开发场景中,读操作非常高频,比写操作的频率高很多,读写锁特别适合于 "频繁读, 不频繁写" 的场景中。synchronized 不是读写锁。


三、轻量级锁 VS 重量级锁


站在加锁操作的开销角度来看,轻量级锁的开销较小,重量级锁的开销较大。


重量级锁的加锁机制严重依赖了操作系统,很容易造成线程的调度,造成大量的内核态和用户态切换(内核态可以想象成银行柜台的工作人员,用户态可以想象成顾客,办理一个业务,频繁的切换工作人员和顾客,这样到最后办理这样一个业务的开销会很大)。而轻量级锁的加锁机制是尽量不会依赖操作系统去切换内核态和用户态。

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁。


四、自旋锁 VS 挂起等待锁

关于自旋锁线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU。这个时候就可以使用自旋锁来处理这样的问题,一直循环等待。

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止。第一次获取锁失败, 第二次的尝试会

在极短的时间内到来。一旦锁被其他线程释放, 就能第一时间获取到锁。

关于挂起等待锁,一旦抢锁失败后就放弃 CPU了,过了一会儿再次获取锁,就算抢锁成功了,此时已经“沧海桑田”了,举个例子:

当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了!

挂起等待锁: 陷入沉沦不能自拔,过了很久很久之后, 突然女神发来消息, "咱俩要不试试?" (注意,

这个很长的时间间隔里, 女神可能已经换了好几个男票了)。

自旋锁: 死皮赖脸坚韧不拔,仍然每天持续的和女神说早安晚安。 一旦女神和上一任分手, 那么就能

立刻抓住机会上位。

自旋锁是一种典型的 轻量级锁 的实现方式:

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁。

缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源。 (而挂起等待的时候是

不消耗 CPU 的)。

synchronized 中的轻量级锁策略可能就是通过自旋锁的方式实现的。


五、公平锁 VS 非公平锁

此处的公平可以理解为“先来后到”的原则,举个例子:

女神正在和舔dog1处对象,但是此时正在排队的有:dog2舔了一年,dog3舔了半年,dog4舔了一个月,dog5舔了一周。当女神和dog1分手后,那么dog2就上位扶正,依次类推。这就是公平锁。

反之,当女神和dog1分手后,dog2、dog3、dog4、dog5随机上位,每个人的机会是公平的,但是这对于舔了很久的dog们来说是不公平的,这就是非公平锁。

操作系统内部的线程调度就可以视为是随机的。 如果不做任何额外的限制, 锁就是非公平锁。如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序。

公平锁和非公平锁没有好坏之分, 关键还是看适用场景。

synchronized 是非公平锁。


六、可重入锁 VS 不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入

(因为这个原因可重入锁也叫做递归锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括

synchronized关键字锁都是可重入的。

或者

不可重入锁:一个线程针对一把锁连续加锁两次,出现死锁。

可重入锁:一个线程针对一把锁,连续加锁多次都不会死锁。



相关文章
|
10天前
|
Java
并发编程的艺术:Java线程与锁机制探索
【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
25 2
|
7天前
|
Java
Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
19 4
|
11天前
|
安全 Java Python
GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
20 6
|
14天前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
11天前
|
调度
线程操作:锁、条件变量的使用
线程操作:锁、条件变量的使用
14 1
|
14天前
|
API
Linux---线程读写锁详解及代码实现
Linux---线程读写锁详解及代码实现
|
1天前
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
12 0
|
1天前
|
安全 Java 开发者
Java并发编程中的线程安全策略
在现代软件开发中,Java语言的并发编程特性使得多线程应用成为可能。然而,随着线程数量的增加,如何确保数据的一致性和系统的稳定性成为开发者面临的挑战。本文将探讨Java并发编程中实现线程安全的几种策略,包括同步机制、volatile关键字的使用、以及java.util.concurrent包提供的工具类,旨在为Java开发者提供一系列实用的方法来应对并发问题。
8 0
|
4天前
|
Java
详尽分享线程池的4种拒绝策略
详尽分享线程池的4种拒绝策略
|
5天前
|
Java
java线程之读写锁
java线程之读写锁
10 0