基础数据
Linux的时间片默认0.75~6ms
Win XP的时间片大约10-15ms左右
可能高可能低,但OS中,时间片的量级在ms级
假设CPU是2GHZ,则每时间片大约对应2M个时钟周期。
两种基本锁
所有锁都是基于这两种基本锁产生的。一些特殊的情况,如乐观锁或是偏向锁,其实更接近于无锁的状态,因而很难用基本锁解释。剩下的所有锁,都是这两种锁的一种实现。
1、 自旋锁
- 原理与互斥锁相反
- 又被称为忙等待(busy-waiting)锁
- 忙碌等待(也称”自旋“;英语:Busy waiting、busy-looping、spinning):一种以【进程反复检查一个条件是否为真】为根本的技术(条件可能为:键盘输入、某个锁是否可用)
- 不过一般来说,忙碌等待是应该避免的反面模式(指的是在实践中经常出现但又低效或是有待优化的设计模式)
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁
- 没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁
- 自旋锁不会改变线程的状态,亦即,自旋锁不会将线程阻塞起来(non-blocking)。
- 机制:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取(循环加锁 -> 等待)。
- 自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换。加锁的过程包含两个步骤:
-
- 第一步:查看锁的状态,如果锁是空闲的,则执行第二步
- 第二步:将锁设置为当前线程持有
- PS:CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
优点:如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
缺点:如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。
场景:适用在等待时间比较短的情况下。此时可以避免OS的进程调度和线程切换。操作系统的内核经常使用自旋锁
解决方案:自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。
例子:JDK在1.6 引入了适应性自旋锁,即自旋时间不固定,而是由前一次在同一个锁上的自旋时间以及锁拥有的状态来决定。基本认为,一个线程上下文切换的时间是最佳的自旋时间。
2、 互斥锁
- 原理与自旋锁相反
- 又被称为无忙等待锁/让权等待锁
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程
- 互斥锁是用于控制多个线程对他们之间共享资源互斥访问的一个信号量。
- 互斥锁加锁失败后会将线程阻塞。
- 这是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。
- 机制:互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本(两次线程上下文切换):
- 【第一次切换】(线程加锁失败)内核把线程的状态从「运行」设置为「睡眠」
- 内核把 CPU 切换给其他线程运行
- (当锁被释放)之前「睡眠」状态的线程会变为「就绪」状态
- 【第二次切换】内核会在合适的时间,把 CPU 切换给该线程运行
- PS:上下切换的耗时大概在几十纳秒到几微秒之间
场景:如果确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。
例子:线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。