线程安全
(一)临界资源与临界区
1、 临界资源
- 能够被多个线程共享的数据/资源,一次仅允许一个进程使用的共享资源。
- 各进程采取互斥的方式,实现共享的资源称为临界资源。
- 属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
2、 临界区
- 对临界资源进行操作的那一段代码。
- 每个进程访问临界资源的那段代码称为临界区,每次只允许一个进程进入临界区,进入后,不允许其他进程进入。
- 不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。多个进程涉及到同一个临界资源的的临界区称为相关临界区。
- 使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。
(二)线程同步与线程互斥
1、线程互斥
- 多个线程之间有共享资源(shared resource)时会出现互斥现象。
- 对于线程A和线程B来讲,在同一时刻,只允许一个线程对临界资源进行操作,即当A进入临界区对资源操作时,B就必须等待;当A执行完,退出临界区后,B才能对临界资源进行操作。
2、 线程同步
- 多线程之间除了有互斥情况外,还有线程同步。
- 当线程A使用某个对象,而此对象又需要线程B修改后才能符合本线程的需要,此时线程A就要等待线程B完成修改工作。这种线程相互等待称为线程的同步。
(三) 线程安全
1、定义
- 线程安全问题都是由全局变量及静态变量引起的。
- 若每个线程中对全局变量、静态变量只有读操作,⽽⽆写操作,⼀般来说,这个全局变量是线程安全的;
- 若有多个线程同时执⾏写操作,⼀般都需要考虑线程同步,否则的话就可能影响线程安全。
2、保证线程安全的方法
保证线程安全的方法:如果你的代码所在的进程中有多个线程在同时运⾏,⽽这些线程可能会同时运⾏这段代码。如果每次运⾏结果和单线程运⾏的结果是⼀样的,⽽且其他的变量的值也和预期的是⼀样的,就是线程安全的
- 加锁
- 为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
- 每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
- 非阻塞同步来实现线程安全
- 原理:先进行操作,如果没有其他线程争⽤共享数据,那操作就成功了;如果共享数据有争⽤,产⽣冲突,那就再采取其他措施(最常⻅的措施就是不断地重试,直到成功为⽌)。
- 这种⽅法需要硬件的⽀持,因为我们需要操作和冲突检测这两个步骤具备原⼦性。
- 线程本地化
- ⼀种⽆同步的⽅案
- 就是利⽤Threadlocal来为每⼀个线程创造⼀个共享变量的副本来(副本之间是⽆关的)避免⼏个线程同时操作⼀个对象时发⽣线程安全问题。
(四) 锁
- 提出背景
- 由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问。
- 由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?
1、 自旋锁(繁忙等待)
- 定义
- 是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,它不用将线程阻塞起来。
- 采用循环加锁 :当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。
- 原理
- 如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
- 适用场景
- 对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
- 如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了
2、 互斥锁(非繁忙等待)
- 定义
- 互斥锁:把自己阻塞起来,等待重新调度请求
- 原理
- 如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何CPU资源),直到第一个线程接触对这个互斥量的锁定为止,第二个线程则被唤醒继续执行,同时锁定这个互斥量。
3、 读写锁
- 定义
- 只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。
- 原理
- 当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;
- 如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;
- 为了避免想要尝试写操作的线程一直得不到写状态锁,当处于读模式的读写锁接收到一个试图对其进行写模式加锁操作时,便会阻塞后面对其进行读模式加锁操作的线程。 即当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。这样当读模式的锁解锁后,要获得写状态锁的线程能够访问此锁保护的资源。所以读写锁非常适合资源的读操作远多于写操作的情况。
4、 悲观锁
- 互斥锁、自旋锁、读写锁都是属于悲观锁
- 悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
5、 乐观锁
- 如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。乐观锁做事比较乐观,它假定冲突的概率很低。
- 原理:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
本文详细梳理了操作系统中的线程安全概念,包括临界资源、临界区和线程同步与线程互斥。介绍了线程安全的定义及保证线程安全的方法,如加锁和线程本地化。同时,深入讨论了锁的类型,如自旋锁、互斥锁、读写锁,以及悲观锁和乐观锁的工作原理和应用场景。
1万+

被折叠的 条评论
为什么被折叠?



