目录
上下文切换
- 上下文切换指的是将当前执行的线程或进程的上下文保存起来,然后切换到另一个线程或进程的上下文,使得它可以继续执行
- 当切换回原来的线程或进程时,之前保存的上下文将被恢复,使得程序可以重新切换回之前的状态继续执行
临界区
- 指多线程或多进程环境下访问共享资源的一段代码区域
- 临界区的目的时确保同时只有一个线程或进程可以进入该区域,并且对共享资源的访问是互斥的
- 是一种用于控制多个线程对共享资源访问的同步机制
- 其本质上就是一个计数器,描述了 可用资源个数
基本思路
- 当线程需要访问共享资源时,首先尝试获取信号量
- 如果信号量的计数器大于零,表示资源可用,线程可以继续执行并将计数器 -1
- 如果信号量的计数器等于零,表示资源已经被占用,线程需要等待,加入到等待队列中
- 当资源被释放时,信号量的计数器 +1,并从等待队列中选择一个线程唤醒,使其继续执行
主要操作
- 初始化:设置信号量的初始计数器值
- P 操作(也称为 wait 操作):申请一个可用资源,如果计数器大于零则 -1,如果等于零,则线程阻塞等待
- V 操作(也称为 signal 操作):释放一个可用资源,且计数器 +1,并唤醒一个等待的线程
使用场景
- 可以用于解决多线程环境下的资源互斥访问和线程同步问题
- 通过适当设置信号量的计数器,可以控制对共享资源的并发访问数量,实现资源的有序访问和互斥访问
注意
- 常用的二元信号量,即只有 0 和 1 两种状态的信号量
- 锁 可以视为是 计数器为 1 的信号量,即二元信号量
- 所以我们可以在代码中使用 Semaphore 来实现类似于锁的效果的,来保证线程安全
- 计数信号量,它的计数器可以是任意正整数,允许多个线程同时访问资源
代码示例
import java.util.concurrent.Semaphore; public class ThreadDemo32 { public static void main(String[] args) throws InterruptedException { // 初始化 Semaphore 对象,并填入相应参数 // 此处 初始化了 3 个可用资源数 Semaphore semaphore = new Semaphore(3); // 使用 acquire 方法执行一次 P 操作 semaphore.acquire(); // 使用 acquire 方法中可填入参数 // 此处 使用 acquire 方法申请了两个资源 semaphore.acquire(2); // 使用 release 方法执行一次 V 操作 semaphore.release(); } }总结
- 信号量本身并不解决竞争条件和死锁问题,而是提供了一种机制来协调和控制线程对共享资源的访问
互斥锁
- 互斥锁是一种独占锁机制
基本思路
- 它确保在任何时刻只有一个线程可以获得锁,其他线程需要等待锁的释放
- 当一个线程获得互斥锁后,它可以访问共享资源并执行相应的操作,其他线程则被阻塞,直到锁被释放
优点
- 确保了共享资源的独占访问问,避免了数据竞争和不一致的问题
适用场景
- 共享资源需要被互斥访问,即同一个时间只能有一个线程访问
- 锁竞争激烈的情况下,只有一个线程可以获得锁
读写锁
- 读写锁是一种共享锁机制
基本思路
- 它允许多个线程同时对共享资源进行读操作,但在进行写操作时需要互斥访问
特点
- 多个线程可以同时获取读锁,实现共享读取
- 写锁是独占的,当一个线程持有写锁时,其他线程无法获取读锁或写锁
优点
- 读写锁在读操作较多、写操作较少的场景下可以提供更高的并发性
- 多个线程可以同时进行读操作,提高了系统的并发能力
适用情况
- 共享资源的读操作远远多于写操作
- 写操作对共享资源的修改需要互斥访问,以确保数据的一致性
- 站在锁冲突概率的预测角度
乐观锁
- 它认为在大多数情况下,并发访问不会发生冲突
基本机制
- 当使用乐观锁时,一般会采用 版本号 或 时间戳 等机制来检查并发冲突
- 在更新资源共享之前,会先读取当前的 版本号 或 时间戳,并在更新时再次检查是否发生了变化
- 如果发生了变化,说明其他线程或用户已经修改了资源,当前操作可能会导致数据不一致
- 因此需要重写读取最新的资源并重新尝试更新操作
缺点
- 乐观锁在处理并发冲突时,通常不会阻塞其他线程或用户的访问,而是允许并发操作,并在冲突发生时进行回滚或重试
- 这增加了系统设计和实现的复杂性,需要考虑并发操作的正确性、一致性、异常处理等方面
总结
- 乐观锁就是指锁冲突的概率不高,因此做的工作就可以简单一些,因此性能也比较高,但往往不能处理到所有问题,需要一定的系统复杂度来应对这些情形
悲观锁
- 它认为在并发环境下,会发生竞争和冲突
基本机制
- 悲观锁的经典实现是使用 互斥锁 或 信号量
- 在使用悲观锁的默认情况下,当一个线程或用户要访问共享资源时,他会先获取锁,阻止其他线程或用户对资源进行修改,直到自己完成操作后才会释放锁
- 这样可以确保同一时间只有一个线程或用户可以访问共享资源,从而避免了并发冲突
总结
- 悲观锁会阻塞其他线程或用户的访问,以确保数据的安全性,但性能相对较低
- synchronized 关键字就是一个典型的悲观锁机制
- 站在加锁开销的角度
轻量级锁
- 加锁机制尽可能不通过系统调度来进行用户态和内核态的切换,而是尽量在用户态完成,实在不行再切换,即典型的纯用户态加锁逻辑,开销较小
核心思想
- 在没有锁竞争时,使用 CAS 操作将对象头部的一部分标记为锁标记(锁记录),而不是直接使用 互斥锁 来实现
- 当线程尝试获取轻量级锁时,它会使用 CAS 操作尝试将锁标记变成自己的线程ID
- 如果 CAS 操作成功,表示该线程成功获取到锁,可继续执行临界区代码
- 如果 CAS 操作失败,表示有其他线程竞争锁,此时会升级为重量级锁
优点
- 在无竞争的情况下性能较好
- 因为它避免了线程阻塞和唤醒的开销,减少了上下文切换的代价
缺点
- 在有竞争的情况下,会频繁进行 CAS 操作,如果 CAS 操作一直失败,会升级为重量级锁,导致性能下降
总结
- 轻量级锁适用于竞争不激烈的情况,在无竞争的情况下性能较好
重量级锁
- 重量级锁是传统的线程同步机制,通常使用 互斥量 或 信号量 实现
核心思想
- 当一个线程获取重量级锁时,如果锁已经被其他线程占用,该线程会被阻塞,进入睡眠状态,直到获取到锁的线程释放锁并唤醒等待线程
优点
- 在有竞争的情况下可以确保线程的正确同步,不会出现数据竞争的问题
缺点
- 在线程切换和阻塞唤醒的过程中存在比较大的开销,包括切换上下文、线程调度、内核态于用户态切换等,会降低系统的性能
总结
- 重量级锁适用于竞争激烈的情况,能确保线程正确同步,但性能相对较低
站在线程 加锁快慢 的角度
自旋锁
- 是一种典型的轻量级锁实现方式
基本思路
- 自旋锁是一个 忙等 的锁机制
- 线程尝试在回去锁时,不会立即进入睡眠状态,而是通过循环不断地检查锁的状态,直到获取到锁为止
- 自旋锁通常使用原子操作(CAS 操作)来实现
优点
- 避免了线程切换和上下文切换的开销,因为线程不会进入睡眠状态
缺点
- 长时间的自旋会占用CPU资源,造成性能损失
- 当线程竞争激烈或持有锁时间比较长时,自旋的效率会下降
适用场景
- 线程持有锁的时间较短,不会导致其他线程长时间等待
- 线程的竞争不激烈,获取锁的时间短
挂起等待锁
- 是一种典型的重量级锁实现方式
基本思路
- 挂起等待所是一种线程阻塞的锁机制
- 线程在尝试获取锁时,如果锁已经被其他线程占用,该线程会进入睡眠状态,释放 CPU 资源,直到锁被释放并且被唤醒后再重新尝试获取锁
优点
- 可以有效避免自旋锁的性能问题,因为线程再等待锁时会释放 CPU 资源,不会占用过多的 CPU 时间
缺点
- 线程的阻塞和唤醒需要操作系统的支持,会引入额外的开销
- 线程的阻塞和唤醒可能会引起上下文切换,影响系统的性能
适用场景
- 线程竞争激烈,可能会有较长的等待时间
- 线程持有锁时间较长,不希望占用 CPU 资源
- 站在线程 获取锁 的角度
公平锁
- 指多个线程在竞争锁时,按照它们发出请求的顺序来获取锁
基本思路
- 当一个线程请求获取锁时,如果锁是可用的,该线程会直接获取锁
- 如果锁已经被其他线程占用,该锁会进入等待队列,等待锁的释放
- 当锁被释放时,等待时间最长的线程 会被唤醒并获取锁
优点
- 确保了锁的获取按照请求的顺序进行,避免了线程饥饿现象
- 所有线程都有公平竞争的机会
适用场景
- 对线程的公平性有较高的要求,希望避免线程饥饿现象
非公平锁
- 指多个线程在竞争锁时,不考虑它们发出请求的顺序,直接尝试获取锁
基本思路
- 当一个线程请求获取锁时,如果线程时可用的,该线程会直接获取锁
- 如果锁已经被其他线程占用,该线程会进入竞争,与其他线程一起竞争锁的所有权
缺点
- 可能会导致某些线程长时间等待,产生线程饥饿现象
适用场景
- 追求更高的系统吞吐量,并且对线程的公平性要求不高
总结
- 操作系统和 synchronized 原生都是 "非公平锁"
- 操作系统对这里的针对加锁的控制,本身就依赖系统调度顺序的
- 这个调度顺序是随机的,不会考虑到这个线程等待锁多久了
- 要想实现公平锁,就得在这个基础上,引入一个队列,让这些想加锁的线程去排队
- 站在 线程是否能重复获取同一个锁的 角度
可重入锁
- 指同一个线程可以多次获取同一个锁而不会造成死锁
基本思路
- 当线程第一次获取锁后,锁会记录该线程的持有者和持有计数
- 在该线程持有锁的期间,它可以再次获取锁而不会被阻塞,而是增加持有计数
- 当线程释放锁时,持有计数递减,直到计数为零时锁完全被释放
优点
- 方便了对共享资源的嵌套访问
- 如果一个线程已经获取了某个锁,那么在持有这个锁的期间,它可以安全的调用其他需要获取通过一个锁的代码
不可重入锁
- 指同一个线程在持有锁的情况下,再次获取锁时会被阻塞
基本思路
- 不可重入锁的一个典型例子是简单的互斥锁,它只允许一个线程在任意时刻获取锁
- 如果一个线程已经持有锁,再次请求获取锁时会被阻塞,直到锁被释放
缺点
- 使用不够方便,在编写复杂的嵌套代码结构时可能会导致死锁和其他问题
总结
- 可重入锁是更常见和推荐的选择