1.自旋锁介绍
自旋锁是这样一类锁:当线程等待加锁时,不会阻塞,不会进入等待状态,而是保持运行状态。大致的思路是:让当前线程不停地的在循环体内执行,当循环的条件被其他线程改变时才能进入临界区。
2.自旋锁实现方式
一种实现方式是通过CAS原子操作:设置一个CAS原子共享变量,为该变量设置一个初始化的值;加锁时获取该变量的值和初始化值比较,若相等则加锁成功,让后把该值设置成另外一个值;若不相等,则进入循环(自旋过程),不停的比较该值,直到和初始化值相加锁成功。
3.自旋锁的优势
(1)性能较高:自旋锁不会使线程状态切换,始终处于用户态,即线程始终处于活动状态,不会让线程进入阻塞状态,减少不必要的上下文切换,性能较高;
(2)避免死锁:自旋锁不会让线程阻塞或等待,也就不需要唤醒,所以可以避免产生死锁;
4.自旋锁的缺点
(1)在等待锁时进入循环会占用CPU,若等待的线程很多,对CPU的消耗会比较大;
(2)不适合需要长时间等待的任务或线程;
(3)不适合大量线程等待的场景。
5.自旋锁的使用场景
(1)等待时间比较短的任务中;
(2)线程数量不太多的应用中;
(3)当等待时间长或线程数量很大时,可以使用其他锁(比如:可重入锁)。
6.自旋锁和互斥锁
(1)自旋锁和互斥锁都是保护资源共享的机制;
(2)无论是自旋锁还是互斥锁,任何时候最多只能有一个持有者;
(3)如果锁已被占用,则获取互斥锁的线程将进入阻塞状态;获取自旋锁的线程不会阻塞。
7.自旋锁的实现
使用AtomicReference变量的CAS机制来实现自旋锁。由于AtomicReference变量能够保证多个线程同时对其读写时的原子性(这种原子性是通过sun.misc.unsafe包来实现的,后面会专门介绍),所以,可以使用这种类型 的共享变量作为判断条件。
public class SpinLock {
// 定义一个原子引用变量
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
// 加锁时:若sign为null,则设置为current;若sihn不为空,则进入循环,自旋等待;
while(!sign.compareAndSet(null, current)){
// 自旋:Do Nothing!!
}
}
public void unlock (){
Thread current = Thread.currentThread();
// 解锁时:sign的值一定为current,所以直接把sign设置为null。
// 这样其他线程就可以拿到锁了(跳出循环)。
sign.compareAndSet(current, null);
}
}
注意:这种自旋锁的实现方式是不可重入的。也就是说:若一个已经加锁成功的线程再次获取该锁时,会失败。那么如何实现一个可重入的自旋锁呢?其实就是比较目前的线程引用是否和锁中记录的线程引用相等,若相等加锁成功。可重入自旋锁会在下面章节进行分析。
8.可重入自旋锁
当一个使用自旋锁加锁成功的线程,再次尝试加锁时可以加锁成功(可重入)。
第1种实现方式
通过ReentrantLock类来实现。ReentrantLock的tryLock不会让线程阻塞,从而可以用来当做自旋锁的对比判断然后循环等待的条件。
import java.util.concurrent.locks.ReentrantLock;
public class SpinLock extends ReentrantLock{
public SpinLock() {
super();
}
public void lock() {
while(!super.tryLock()) {
// Do Nothing;自旋
}
}
public void unlock() {
super.unlock();
}
}
第2种实现方式
通过AtomicReference对象来实现。
注意:同一个线程多次加锁可重入,解锁只需要调用一次。若多次调用解锁函数,只有第一次解锁成功,后续的解锁操作无效。
以上的实现逻辑是一种修改过的逻辑,更加通用的实现逻辑是:加锁的调用次数和解锁的调用次数相等。
public class ReentrantSpinLock {
private AtomicReference<Thread> sign = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 若尝试加锁的线程和已加的锁中的线程相同,加锁成功
if (current == sign.get()) {
return;
}
//If the lock is not acquired, it can be spun through CAS
while (!sign.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
// 锁的线程和目前的线程相等时,才允许释放锁
if (cur == sign.get()) {
sign.compareAndSet(cur, null);
}
}
}
}
以上实现需要注意的几个地方:
(1)没有对加锁的次数进行计数,也就是说加锁和解锁与调用lock的次数无关,也就是说:
(2)当同一个线程多次加锁时,都可以加锁成功,但只算一次;
(3)当同一个加锁成功的线程想要解锁时,只需要调用一次unlock函数。
这里也可以通过计数来实现一个,解锁次数和加锁次数相等时解锁才成功,否则解锁不成功的设计。
使用自旋锁
以下是在多线程中使用自旋锁的例子。
class Worker implements Runnable {
private ReentrantSpinLock slock = new ReentrantSpinLock();
public void run() {
slock.lock();
slock.lock(); // 按以上实现,这一句什么也不做
for (int i = 0; i < 10; i ++) {
System.out.printf("%s,", Thread.currentThread().getName());
}
System.out.println("");
slock.unlock();
slock.unlock(); // 按以上实现,若解锁成功,这一句什么也不做
}
}
public class SpinLockTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable worker = new Worker();
for (int i = 0; i < 2; i++) {
executor.submit(worker);
}
executor.shutdown();
while (!executor.isTerminated()){ }
}
}
从输出结果可以看到,自旋锁生效了。这种自旋锁的实现实际上是一种折中,因为若是依赖于加锁的次数,当加锁次数太多时,容易造成死锁。所以,这里并没有依赖加锁次数。