CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
1 package org.fool.hellojava.lock;
2
3 import java.util.concurrent.atomic.AtomicReference;
4
5 public class SpinLockTest {
6 private static SpinLock lock = new SpinLock();
7
8 public static void main(String[] args) {
9 new Thread(() -> {
10 lock.lock();
11
12 test();
13
14 lock.unlock();
15 }).start();
16
17 new Thread(() -> {
18 lock.lock();
19
20 test();
21
22 lock.unlock();
23 }).start();
24
25 new Thread(() -> {
26 lock.lock();
27
28 test();
29
30 lock.unlock();
31 }).start();
32 }
33
34 public static void test() {
35 System.out.println(Thread.currentThread().getName() + " invoked test...");
36
37 try {
38 Thread.sleep(500);
39 } catch (InterruptedException e) {
40 e.printStackTrace();
41 }
42 }
43
44 private static class SpinLock {
45 private AtomicReference<Thread> cas = new AtomicReference<>();
46
47 public void lock() {
48 Thread currentThread = Thread.currentThread();
49
50 while (!cas.compareAndSet(null, currentThread)) {
51 System.out.println(Thread.currentThread().getName() + " waiting...");
52 }
53 }
54
55 public void unlock() {
56 Thread currentThread = Thread.currentThread();
57 cas.compareAndSet(currentThread, null);
58 }
59
60 }
61 }
lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
自旋锁的缺点
1.如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2.上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
Java实现可重入自旋锁
为了实现可重入锁,需要引入一个计数器,用来记录获取锁的线程数。
1 package org.fool.hellojava.lock;
2
3 import java.util.concurrent.atomic.AtomicReference;
4
5 public class ReentrantSpinLockTest {
6
7 private static ReentrantSpinLock lock = new ReentrantSpinLock();
8
9 public static void main(String[] args) {
10 new Thread(() -> {
11 lock.lock();
12
13 test();
14
15 lock.unlock();
16 }).start();
17 }
18
19 public static void test() {
20 lock.lock();
21 System.out.println(Thread.currentThread().getName() + " invoked test...");
22
23 try {
24 Thread.sleep(5000);
25 } catch (InterruptedException e) {
26 e.printStackTrace();
27 }
28
29 lock.unlock();
30 }
31
32 private static class ReentrantSpinLock {
33 private AtomicReference<Thread> cas = new AtomicReference<>();
34 private int count;
35
36 public void lock() {
37 Thread currentThread = Thread.currentThread();
38
39 if (currentThread == cas.get()) {
40 count++;
41 return;
42 }
43
44 while (!cas.compareAndSet(null, currentThread)) {
45 // DO nothing
46 System.out.println(Thread.currentThread().getName() + " waiting...");
47 }
48 }
49
50 public void unlock() {
51 Thread currentThread = Thread.currentThread();
52 if (currentThread == cas.get()) {
53 if (count > 0) {
54 count--;
55 } else {
56 cas.compareAndSet(currentThread, null);
57 }
58 }
59 }
60 }
61 }
自旋锁与互斥锁
1.自旋锁与互斥锁都是为了实现保护资源共享的机制。
2.无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
3.获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
自旋锁总结
1.自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
2.自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
3.自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
4.自旋锁本身无法保证公平性,同时也无法保证可重入性。
5.基于自旋锁,可以实现具备公平性和可重入性质的锁。