一、 为什么会有线程安全问题
1)程序和CPU间的协作关系
- CPU组成
- 寄存器
- 存储了从内存加载的数据(从内存中将数据加载到 L1,L2,L3 缓存,再到寄存器) ;
- 寄存器的运行速度比内存快好多个级别;
- 所编写的程序在计算机底层转换为汇编语言后,主要操控对象是寄存器;
- 按存储数据分类
- 内存地址类寄存器
- 程序计数器、基址寄存器,变址寄存器
- 非内存地址寄存器
- 累加寄存器,通用寄存器,标志寄存器
- 内存地址类寄存器
- 寄存器
i ++ 问题:
多个线程同时使用i++指令的时候就可能会出现如下图的冲突,不同线程在执行的时候,各自的 eax 累加寄存器中的数值不相同,从而导致最终i被两次更新,但值却不是 2。
解决此类问题:只需要保证每个线程在执行i++ 指令的时候都是一个原子操作即可,例如通过加入一道屏障指令;
当线程 a 将变量读取到寄存器中,就给变量加一道屏障;
信号量
1)是什么:信号量其实是一种设计思想,它的本质可以理解为是一个整形的数字(sem),对于这个数字的访问,在具体实现上只提供两个原子操作
1)共享资源块是指被多个线程或进程共同访问和操作的一段代码或数据区域
2)原理(为什么)
p():prolaag 减少
,又称为 wait 或 down 操作;主要作用是对信号量的计数器 -1(即 sem - 1 操作),如果计数器值为 0,则线程将被阻塞,后续的请求会被放置到一条等待队列中;当有请求抵达灵界区时,会触发 p 操作;
v():verhoog 增加
,又称为signal操作或up操作;主要作用是对信号量的计数器进行 +1(即 sem + 1 操作),如果存在被阻塞的线程,则唤醒其中一个线程;
其中,计数器初始化值为信号量的初始资源数目;随着之前在临界区的访问处理结束后,就会触发一次 v 操作,然后从等待队列的队头通知一个之前处于等待状态的请求,让其进入临界区;
3)如何使用
1、多进程间任务执行的先后顺序
A 进程的 method_3 需要在 B 进程执行 method_2 后运行:保证在执行 A 进程 method_3 之前都执行 B 进程的 method_2 -> 执行 method_3 时需要借助 B 进程发起 V 操作;
2、多进程间任务的前驱关系
3、通过信号量实现访问次数限制
管程
- 英文名字:monitor(监视器)
- 对信号量的基础做一层封装;专用于访问一些共享变量的函数,使调用方使用起来更加简单和清晰;作用为一次只允许一个线程访问临界区,如果同时有多个访问,则将多余的访问挂起;
- 作用:管程的提出 , 则是用在了语言的场景中,它主要是专门针对语言中的并发场景而设计的,从而简化了一些语言层面的实现逻辑。例如 Java 内部对于并发模块的实现 wait()、notify()、notifyAll() 这些函数就是采用了管程技术来控制的。
- 组成
- 一把锁:管程为了保证对于共享资源的访问一次只能有一个进程,所以引入了锁的机制,没有抢到锁的进程则需要进入到锁的等待队列进行等待;
- 0 或多个条件变量:当进程获取到了锁的请求进入到临界区之后,有可能还需要做多个条件的判断,如果没有满足其中的某一条条件则需要进入到条件队列(条件队列位于临界区外)中,若后续条件满足,则由其他进程进行唤醒。
- 什么临界区:临界区(Critical Section)是访问共享资源时的一段代码区域
管程模型实现生产者消费者模型