自旋锁原理
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值(1.6引入了适应性自旋锁)
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果CPU处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。
一个简单的例子
public class SpinLock {
private AtomicReference cas = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
复杂的例子
public class SpinLockDemo {
//原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t comeIn");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t myUnlock ");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
}, "t2").start();
}
}
用版本的自旋
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* Function:自旋锁演示demo
*
* A线程加锁后沉睡5秒然后解锁,B线程加锁解锁
*
* 请手写一个自旋锁
*/
public class SpinLockDemo {
//定一个一个带时间戳的原子线程类,给的默认值是false
static AtomicStampedReference<Thread> atomicThread = new AtomicStampedReference<>(null,1);
public static void main(String args[]){
new Thread(()->{
myLock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
myUnLock();
}
},"AAA").start();
new Thread(()->{
myLock();
myUnLock();
},"BBB").start();
}
public static void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+" come in,version is "+atomicThread.getStamp());
while(!atomicThread.compareAndSet(null,thread,1,2)){
// System.out.println("加锁失败");
}
System.out.println(thread.getName()+"加锁成功");
System.out.println(thread.getName()+" come out,version is "+atomicThread.getStamp());
}
public static void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+" come in,version is "+atomicThread.getStamp());
while(!atomicThread.compareAndSet(thread,null,2,1)){
// System.out.println("解锁失败");
}
System.out.println(thread.getName()+"解锁成功");
System.out.println(thread.getName()+" come out,version is "+atomicThread.getStamp());
}
}
输出结果
AAA come in,version is 1
BBB come in,version is 1
AAA加锁成功
AAA come out,version is 2
AAA come in,version is 2
AAA解锁成功
BBB加锁成功
BBB come out,version is 2
BBB come in,version is 2
BBB解锁成功
BBB come out,version is 1
AAA come out,version is 2
由此可见,B线程进入myLock的时候atomicThread的版本号为2,显然对于B线程来说是无法进行加锁的,无法加锁只能一直循环循环循环,询问A线程到我了吗到我了吗,A线程沉睡五秒后开始解锁,解锁完毕之后,B线程第n次循环之后再来问到我了吗,A线程告诉B到你了,然后B开始加锁,加锁完了之后再开始解锁。在A完成解锁之前B线程的期望值跟期望版本号一直都不是跟内存中存在的值与版本号对应,所以,B一直都是处于循环等待的状态,等A完事儿之后,期望值跟期望版本号跟内存中的值跟版本号都一样了,B线程才开始加锁,B线程进入mylock的时候版本号是2,出来的时候版本号是1。这就是很明显的自旋。