1、什么是自旋
所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。
自旋其实就是在当前这个线程获取同步资源锁失败的时候,该线程会在原地一直等待锁释放,不会把该线程阻塞,只要获得锁的那个线程释放锁之后,这个等待的线程马上就可以去获得锁。原地循环等待会占用处理器时间的,类似在执行一个空的for循环一样。
2、非自旋锁
非自旋锁尝试获取同步资源锁失败,会阻塞再唤醒一个Java线程。互斥锁就是一种非自旋锁。
我们通过一个打水的图来梳理一下非自旋锁竞争资源的流程:
3、自旋锁
在有些时候,同步代码块中的内容比较简单,状态转换消耗的时间有可能比用户代码执行的时间还要长,想想就像为了吃个方便面,要让我到五公里远的小卖部接热水一样,得不偿失。
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
当线程在竞争锁的时候,结果竞争不过人家,被阻塞的话,但我想着要是刚刚那个线程马上就用完了呢,去睡觉的话,刚睡着(阻塞)被叫醒(唤醒)还不说,那得多麻烦浪费时间(耗费cpu性能)呀,我还是自旋等一下看看吧。
自旋锁在锁持有时间长,锁竞争不激烈的情况下更能突出性能。
这里也画了一个打水的图来理解自旋锁:
优点:
- 自旋锁减少了线程在阻塞和唤醒之间切换的频率,那么就是降低了操作系统在用户态和内核态的切换,降低了cpu的损耗,提高了线程在竞争期间的性能。
缺点:
- 在单核处理器中,并不存在所谓的并行,所以说当线程在占据cpu时,由于时间片轮转运行到等待线程时,等待线程在那自旋没有用的,因为线程此时没有办法释放锁,那么此时自旋就会很多余,当时现在的电脑基本都是4核,6核还有8核的,所以这个问题在以前存在,现在基本还好。
- 自旋锁的时间无法判断,如果线程持有锁的时间短,小于或者等于自旋的时间,那么就很舒服,自旋过了刚好获得了锁,但是很多时候这个锁持有时间长短就要看代码设计了,所以无法预测,这就很难受。基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
这些问题又在1.6引入了自适应自旋锁之后得到了优化
4、适应性自旋锁
所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数
java在jdk1.6之后引入了自适应自旋锁,主要是为了解决自旋锁自旋时间不合理问题的。对于之前的自旋锁而言,自旋时间不好控制,而且分配也不合理,很可能owner线程很快就释放锁了,但是就因为自旋时间不合理错过了而导致cpu在用户态和内核态切换。
自适应自旋锁是根据上一个线程竞争这个锁所需要的时间来决定当前线程自旋的时间。对于操作系统来说,上一个线程竞争锁所需要的时间极有可能是这次所需的时间,所以自旋一般控制在10-100之内。如果上一个线程很快就获得了锁,那么就认为很快就能获得锁,所以就允许自旋时间长一点,比如100个循环;如果上一个线程很久才获得锁,那么就认为很难获得锁,就让自旋时间短一点或者直接阻塞,以免多余浪费cpu。自旋后仍然失败,那么就阻塞当前线程。
优点:
- 优点很明显,优化了自旋时间不合理的问题,动态分配自旋时间。
缺点
- 依旧没有完全解决时间不合理问题