自旋锁是这样一类锁:当线程等待加锁时,不会阻塞,不会进入等待状态,而是保持运行状态。大致的思路是:让当前线程不停地的在循环体内执行,当循环的条件被其他线程改变时才能进入临界区。

一种实现方式是通过CAS原子操作:设置一个CAS原子共享变量,为该变量设置一个初始化的值;加锁时获取该变量的值和初始化值比较,若相等则加锁成功,让后把该值设置成另外一个值;若不相等,则进入循环(自旋过程),不停的比较该值,直到和初始化值相加锁成功。

自旋锁的优势

(1)性能较高:自旋锁不会使线程状态切换,始终处于用户态,即线程始终处于活动状态,不会让线程进入阻塞状态,减少不必要的上下文切换,性能较高;

(2)避免死锁:自旋锁不会让线程阻塞或等待,也就不需要唤醒,所以可以避免产生死锁;

自旋锁的缺点

(1)在等待锁时进入循环会占用CPU,若等待的线程很多,对CPU的消耗会比较大;

(2)不适合需要长时间等待的任务或线程;

(3)不适合大量线程等待的场景。

自旋锁的使用场景

(1)等待时间比较短的任务中;

(2)线程数量不太多的应用中;

(3)当等待时间长或线程数量很大时,可以使用其他锁(比如:可重入锁)。

自旋锁和互斥锁

(1)自旋锁和互斥锁都是保护资源共享的机制;

(2)无论是自旋锁还是互斥锁,任何时候最多只能有一个持有者;

(3)如果锁已被占用,则获取互斥锁的线程将进入阻塞状态;获取自旋锁的线程不会阻塞。

自旋锁的实现

使用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);
  }
}

注意:这种自旋锁的实现方式是不可重入的。也就是说:若一个已经加锁成功的线程再次获取该锁时,会失败。那么如何实现一个可重入的自旋锁呢?其实就是比较目前的线程引用是否和锁中记录的线程引用相等,若相等加锁成功。可重入自旋锁会在下面章节进行分析。

可重入自旋锁

当一个使用自旋锁加锁成功的线程,再次尝试加锁时可以加锁成功(可重入)。

第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()){   }
    }
}

从输出结果可以看到,自旋锁生效了。这种自旋锁的实现实际上是一种折中,因为若是依赖于加锁的次数,当加锁次数太多时,容易造成死锁。所以,这里并没有依赖加锁次数。

小结

本文介绍了不可重入和可重入自旋锁,并对其实现进行了介绍。

转载至 https://blog.csdn.net/zg_hover/article/details/121617050
尊重原创!!!