互斥锁(Mutex)和自旋锁(Spinlock)是两种常见的用于多线程编程中的同步机制,用于确保在多个线程访问共享资源时的协调性和正确性。以下是它们的主要特点和区别:
互斥锁(Mutex)
定义:
互斥锁是一种阻塞锁,当某个线程持有锁时,其他试图获取该锁的线程将被阻塞,直到锁被释放。
特性:
原子性:互斥锁的获取和释放是原子的,确保了在多线程环境下对共享资源的安全访问。
休眠等待:当线程无法获取互斥锁时,它会被操作系统挂起并进入休眠状态,不再消耗CPU资源。
上下文切换:线程被挂起时,操作系统会进行上下文切换,以便其他线程得以执行。
适用场景:
适用于锁被持有时间较长或预计等待时间超过两次线程上下文切换时间的场景。
自旋锁(Spinlock)
定义:
自旋锁是一种非阻塞锁,当线程尝试获取一个被其他线程持有的自旋锁时,该线程会进入忙等待(busy-waiting)状态(一直轮询),即不断地循环检查锁是否可用,而不是被挂起。
特性:
非阻塞:线程在无法获取自旋锁时,不会进入休眠状态,而是持续消耗CPU资源进行忙等待。
无上下文切换:由于线程没有进入休眠状态,因此不会发生上下文切换。
参数限制:为了防止无限循环导致CPU资源耗尽,自旋锁通常有一个参数来限制最大尝试次数。
适用场景:
多核处理器:在多核处理器环境中,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。
保持锁时间短:自旋锁比较适用于锁使用者保持锁时间比较短的情况。
中断上下文:如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),则必须使用自旋锁。
互斥锁与自旋锁的比较
资源消耗:互斥锁在线程等待时会释放CPU资源,而自旋锁则会持续消耗CPU资源进行忙等待。
上下文切换:互斥锁在线程等待时会导致上下文切换,而自旋锁则没有。
适用场景:互斥锁适用于锁被持有时间较长或预计等待时间较长的场景;自旋锁适用于锁被持有时间较短或预计等待时间较短的场景。
性能:在多核处理器环境中,如果预计等待时间很短,自旋锁的性能可能优于互斥锁;但在单核处理器或预计等待时间较长的情况下,互斥锁的性能可能更好。
死锁(Deadlock)
死锁(Deadlock) 是在并发系统中,两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
例如:如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。这样就会造成的一种互相等待的现象。
死锁发生的原因主要有以下几点:
互斥条件:资源是独占的且排他的使用,进程互斥使用资源,即任一时刻只有一个进程能访问该资源(如打印机、磁带机等)。
请求与保持条件:已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的资源保持不放。
不可剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被链中下一个进程所请求。
互斥锁中,当锁被释放,等待锁释放的线程是如何知道?
当某个线程持有互斥锁时,其他试图获取该锁的线程将被阻塞,直到锁被释放。一旦锁被释放,被阻塞的线程如何得知锁已可用并继续执行,这涉及到操作系统和互斥锁实现的具体机制。
阻塞队列:
当多个线程试图获取同一把互斥锁,而没有获取到锁的线程会被组织到一个阻塞队列(BlockingQueue)中。
这个阻塞队列是由操作系统或互斥锁实现维护的,用于存储等待锁的线程。
锁释放:
当持有锁的线程完成对共享资源的访问并调用解锁操作时,互斥锁的状态会发生变化,表示锁现在处于可用状态。
通知机制:
一旦锁被释放,操作系统或互斥锁实现会负责通知阻塞队列中的一个或多个线程。
通知的方式通常依赖于底层操作系统的调度机制,比如通过信号、中断或其他同步原语。
线程唤醒:
收到通知的线程会从阻塞状态变为就绪状态(Ready State),等待操作系统的调度。
操作系统会根据其调度策略(如先来先服务、优先级调度等)来决定何时调度该线程到CPU上执行。
重新获取锁:
当线程被操作系统调度到CPU上执行时,它会再次尝试获取互斥锁。
如果此时锁仍然可用(即没有其他线程持有它),则该线程会成功获取锁并继续执行其临界区代码。
防止饥饿:
为了确保公平性,操作系统或互斥锁实现通常会采用某种策略来防止饥饿(Starvation)现象,即确保等待时间较长的线程能够有机会获取到锁。
需要注意的是,具体的实现细节可能因操作系统、编程语言或线程库的不同而有所差异。但总体来说,互斥锁通过阻塞队列、通知机制和线程调度等机制来实现线程间的同步和协调。