自旋锁是并发编程实战里面一个关于锁优化的非常重要的一个概念,通常情况下会配合CAS原语来实现轻量级的同步操作。
自旋锁一般用于线程竞争不激烈,临界区代码非常少,并且执行操作非常快的场景下。本质上是为了减少线程调度的上下文切换时间。所以在访问临界区资源失败的情况下并不会立即进入BLOCK状态,而通常是会再循环一定的cpu周期或时间直到该线程可以获得锁条件,
自旋的目的就是为了减少上下文线程调度的切换时间,从而会空转几个cpu周期,如果在此时间内,在此获取了锁便可以直接运行,从而避免上下文频繁调度。
自旋锁的缺点:
(1)如果线程竞争激烈会导致一些自旋cpu周期过长,从而浪费了大量的cpu资源
(2)自旋锁本身是抢占式的加锁,从而可能导致有些线程会出现饥饿现象,也就是所谓的不公示特证。
非公司自旋锁的示例如下:
package concurrent.lock_compare;
import java.util.concurrent.atomic.AtomicReference;
/**
* Created by Administrator on 2018/8/2.
*/
public class SpinLock {
private AtomicReference<Thread> owner=new AtomicReference<>();
private void lock() throws InterruptedException {
Thread expectValue=null;
Thread updateValue;
do {
updateValue=Thread.currentThread();
System.out.println(updateValue.getName()+" 自旋等待中..... ");
Thread.sleep(1000);
//只有第一个执行的线程,才会加锁成功,其他一直处于自旋等待中
}while (!owner.compareAndSet(expectValue,updateValue));
System.out.println(updateValue.getName()+" 加锁成功...... 4秒后释放锁 ");
// do work
Thread.sleep(4000);
System.out.println(updateValue.getName()+" 释放锁了。。。。。 ");
unlock();
}
public void unlock(){
Thread expectValue=Thread.currentThread();
owner.compareAndSet(expectValue,null);
}
public static void main(String[] args) {
SpinLock spinLock=new SpinLock();
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
spinLock.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(runnable);
Thread t2=new Thread(runnable);
Thread t3=new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
输出结果:
Thread-0 自旋等待中.....
Thread-2 自旋等待中.....
Thread-1 自旋等待中.....
Thread-2 自旋等待中.....
Thread-1 自旋等待中.....
Thread-0 加锁成功...... 4秒后释放锁
Thread-2 自旋等待中.....
Thread-1 自旋等待中.....
Thread-2 自旋等待中.....
Thread-1 自旋等待中.....
Thread-1 自旋等待中.....
Thread-2 自旋等待中.....
Thread-0 释放锁了。。。。。
Thread-1 加锁成功...... 4秒后释放锁
Thread-2 自旋等待中.....
Thread-2 自旋等待中.....
Thread-2 自旋等待中.....
Thread-2 自旋等待中.....
Thread-1 释放锁了。。。。。
Thread-2 加锁成功...... 4秒后释放锁
Thread-2 释放锁了。。。。。
实现公平的自旋锁:
实现一个公平的自旋锁,其实也比较容易,我们只需要按照线程的程序,构建一个FIFO先进先出的阻塞队列,便可以完成这件事。
一个生活中的例子是:我们去银行办业务,到了之后通常会先取一个号,然后坐等柜台叫号或者自己主动去看大屏幕上的号是否到我们了,当柜台每次处理完一个号,下次叫的号都是上次叫的号的+1,所以取票的顺序就是我们公平处理的顺序,按照这个思路我们来看下实现公平自旋的代码:
package concurrent.lock_compare;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by qindongliang on 2018/8/2.
*/
public class TicketLock {
//当前正在服务的号码
private AtomicInteger serviceNum=new AtomicInteger();
//正在排队的号码
private AtomicInteger ticketNum=new AtomicInteger();
private void lock() throws InterruptedException {
int getTicketNum=ticketNum.getAndIncrement();
do{
System.out.println(Thread.currentThread().getName()+" 开始自旋等待..... ticketNum="+getTicketNum);
Thread.sleep(1000);
}while (getTicketNum!=serviceNum.get());
System.out.println(Thread.currentThread().getName()+" 使用号"+getTicketNum+" 完毕。");
unlock();
}
private void unlock(){
//开始叫下一位的号
int nextServiceNum= 1 + serviceNum.get();
serviceNum.compareAndSet(serviceNum.get(),nextServiceNum);
}
public static void main(String[] args) {
TicketLock ticketLock=new TicketLock();
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
ticketLock.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(runnable);
Thread t2=new Thread(runnable);
Thread t3=new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
输出如下:
Thread-0 开始自旋等待..... ticketNum=0
Thread-2 开始自旋等待..... ticketNum=2
Thread-1 开始自旋等待..... ticketNum=1
Thread-1 开始自旋等待..... ticketNum=1
Thread-2 开始自旋等待..... ticketNum=2
Thread-0 使用号0 完毕。
Thread-2 开始自旋等待..... ticketNum=2
Thread-1 使用号1 完毕。
Thread-2 使用号2 完毕。
从上面的结果我们能看出,服务号从0开始,依次处理,只有上一个服务号处理完毕,才会进行下一个服务号办理。从而就实现了公平的自旋锁模式。
公平的自旋锁能够确保不会出现线程饥饿现象,但公平模式不一定就意味着效率很高,具体跟临界区的代码执行的时长有关,如果临界区是一块很大的逻辑,那么就会导致其它自旋线程耗费大量的cpu资源。
总结:
本文主要了介绍了Java里面自旋锁的公平模式和非公平的实现,并介绍了其相关的优缺点,自旋锁通常搭配CAS来一起工作,自旋锁的临界区代码不能太多,而且耗时要尽可能的短,否则一旦自旋的代价超过线程睡眠唤醒调度的代价,那么将会大大浪费cpu资源。