自旋锁的由来
计算机系统资源总是有限的,有些资源需要互斥访问,因此就有了锁机制,只有获得锁的线程才能访问资源。锁保证了每次只有一个线程可以访问资源。当线程申请一个已经被其他线程占用的锁,就会出现两种情况。一种是没有获得锁的线程会阻塞自己,等到锁被释放后再被唤起,这就是互斥锁;另一种是没有获得锁的线程一直循环在那里看是否该锁的保持者已经释放了锁,这就是自旋锁。
自旋锁的优缺点
首先来对比一下互斥锁和自旋锁。
互斥锁:从等待到解锁过程,线程会从sleep状态变为running状态,过程中有线程上下文的切换,抢占CPU等开销。
自旋锁:从等待到解锁过程,线程一直处于running状态,没有上下文的切换。
自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁。
虽然自旋锁效率比互斥锁高,但它会存在下面两个问题:
1、自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。
2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
Java中自旋锁实现
public class SpinLock {
private AtomicReference sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
}
上述代码使用了CAS原子操作,调用了AtomicReference类的compareAndSet方法。
调用lock方法时,如果sign当前值为null,说明自旋锁还没有被占用,将sign设置为currentThread,并进行锁定。
调用lock方法时,如果sign当前值不为null,说明自旋锁已经被其他线程占用,当前线程就会在while中继续循环检测。
调用unlock方法时,会将sign置为空,相当于释放自旋锁。
总结
由于自旋锁只是在当前线程不停地执行循环体,不进行线程状态的切换,因此响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要占用CPU时间。如果线程竞争不激烈,并且保持锁的时间很短,则适合使用自旋锁。
可重入锁
上一篇文章中讲述了信号量和互斥量,其中互斥量一般用于保证对于资源的互斥访问,和锁的本质一样。本文讲述简单锁的实现和可重入锁的基本原理。
简单锁
在讲述简单锁的实现之前,我们先来看一个锁的应用例子:
public class Counter{
private Lock lock = new Lock();
private int count = 0;
public int inc(){
lock.lock();
this.count++;
lock.unlock();
return count;
}
}
上面的程序中,由于this.count++这一操作分多步执行,在多线程环境中可能出现结果不符合预期的情况,这段代码称之为 临界区 ,所以需要使用lock来保证其原子性。
Lock的实现:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){ //不用if,而用while,是为了防止假唤醒
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
说明:当isLocked为true时,调用lock()的线程在wait()阻塞。 为防止该线程虚假唤醒,程序会重新去检查isLocked条件。 如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。当线程完成了临界区中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且唤醒 其中一个 处于等待状态的线程。
锁的可重入性
同样,先举例来说明锁的可重入性:
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入 。通常也称为 自旋锁 。相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
可重入锁的基本原理
我们在上面的Lock基础上作一些修改:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
lockBy:保存已经获得锁实例的线程,在lock()判断调用lock的线程是否已经获得当前锁实例,如果已经获得锁,则直接跳过while,无需等待。
lockCount:记录同一个线程重复对一个锁对象加锁的次数。否则,一次unlock就会解除所有锁,即使这个锁实例已经加锁多次了。
Java中常用的锁的属性
synchronized:可重入锁;
java.util.concurrent.locks.ReentrantLock:可重入锁;
作者:anytimekaka
链接:http://www.jianshu.com/p/007bd7029faf
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。