Node.js工程师养成打算【某课完结内附材料文档】
下载地址:百度网盘
从零开始自己动手写自旋锁
咱们在写并发程序的时候,一个非常常见的需要就是保障在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保障一个时刻只有一个线程执行临界区的代码的方法就是锁。在本篇文章当中咱们将会认真分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。
原子性
在谈自旋锁之前就不得不谈原子性了。所谓原子性简略说来就是一个一个操作要么不做要么全做,全做的意义就是在操作的过程当中不能够被中断,比方说对变量data进行加一操作,有以下三个步骤:
将data从内存加载到寄存器。
将data这个值加一。
将失去的后果写回内存。
原子性就示意一个线程在进行加一操作的时候,不能够被其余线程中断,只有这个线程执行完这三个过程的时候其余线程才能够操作数据data。
咱们现在用代码体验一下,在Java当中咱们可能使用AtomicInteger进行对整型数据的原子操作:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger data = new AtomicInteger();data.set(0); // 将数据初始化位0Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 对数据 data 进行原子加1操作 }});Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 对数据 data 进行原子加1操作 }});// 启动两个线程t1.start();t2.start();// 等待两个线程执行实现t1.join();t2.join();// 打印最终的后果System.out.println(data); // 200000
}
}
复制代码
从下面的代码分析可能知道,如果是一般的整型变量如果两个线程同时进行操作的时候,最终的后果是会小于200000。
自己动手写自旋锁
AtomicInteger类
现在咱们已经了解了原子性的作用了,咱们现在来了解AtomicInteger类的另外一个原子性的操作——compareAndSet,这个操作叫做比较并交换(CAS),他具备原子性。
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.set(0);
atomicInteger.compareAndSet(0, 1);
}
复制代码
compareAndSet函数的意义:首先会比较第一个参数(对应下面的代码就是0)和atomicInteger的值,如果相等则进行交换,也就是将atomicInteger的值设置为第二个参数(对应下面的代码就是1),如果这些操作胜利,那么compareAndSet函数就返回true,如果操作失败则返回false,操作失败可能是因为第一个参数的值(期望值)和atomicInteger不相等,如果相等也可能因为在更改atomicInteger的值的时候失败(因为可能有多个线程在操作,因为原子性的存在,只能有一个线程操作胜利)。
自旋锁实现原理
咱们可能使用AtomicInteger类实现自旋锁,咱们可能用0这个值示意未上锁,1这个值示意已经上锁了。
AtomicInteger类的初始值为0。
在上锁时,咱们可能使用代码atomicInteger.compareAndSet(0, 1)进行实现,咱们在后面已经提到了只能够有一个线程实现这个操作,也就是说只能有一个线程调用这行代码而后返回true其余线程都返回false,这些返回false的线程不能够进入临界区,因此咱们需要这些线程停在atomicInteger.compareAndSet(0, 1)这行代码不可以往下执行,咱们可能使用while循环让这些线程一直停在这里while (!value.compareAndSet(0, 1));,只有返回true的线程才能够跳出循环,其余线程都会一直在这里循环,咱们称这种行为叫做自旋,这种锁因而也被叫做自旋锁。
线程在出临界区的时候需要从新将锁的状态调整为未上锁的上状态,咱们使用代码value.compareAndSet(1, 0);就可能实现,将锁的状态还原为未上锁的状态,这样其余的自旋的线程就可能拿到锁,而后进入临界区了。
自旋锁代码实现
import java.util.concurrent.atomic.AtomicInteger;
public class SpinLock {
// 0 示意未上锁状态
// 1 示意上锁状态
protected AtomicInteger value;
public SpinLock() {
this.value = new AtomicInteger();// 设置 value 的初始值为0 示意未上锁的状态this.value.set(0);
}
public void lock() {
// 进行自旋操作while (!value.compareAndSet(0, 1));
}
public void unlock() {
// 将锁的状态设置为未上锁状态value.compareAndSet(1, 0);
}
}
复制代码
下面就是咱们自己实现的自旋锁的代码,这看起来实在太简略了,然而它确实帮助咱们实现了一个锁,而且能够在实在场景进行使用的,咱们现在用代码对下面咱们写的锁进行测试。
测试程序:
public class SpinLockTest {
public static int data;
public static SpinLock lock = new SpinLock();
public static void add() {
for (int i = 0; i < 100000; i++) { // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环 lock.lock(); data++; lock.unlock();}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];// 设置100个线程for (int i = 0; i < 100; i ++) { threads[i] = new Thread(SpinLockTest::add);}// 启动一百个线程for (int i = 0; i < 100; i++) { threads[i].start();}// 等待这100个线程执行实现for (int i = 0; i < 100; i++) { threads[i].join();}System.out.println(data); // 10000000
}
}
复制代码
在下面的代码单中,咱们使用100个线程,而后每个线程循环执行100000data++操作,下面的代码最初输入的后果是10000000,和咱们期待的后果是相等的,这就说明咱们实现的自旋锁是正确的。
自己动手写可重入自旋锁
可重入自旋锁
在下面实现的自旋锁当中已经可能满足一些咱们的基本需要了,就是一个时刻只能够有一个线程执行临界区的代码。然而下面的的代码并不能够满足重入的需要,也就是说下面写的自旋锁并不是一个可重入的自旋锁,事实上在下面实现的自旋锁当中重入的话就会产生死锁。
咱们通过一份代码来模拟下面重入产生死锁的情况:
public static void add(int state) throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
if (state <= 3) {
lock.lock();System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);for (int i = 0; i < 10; i++) data++;add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区lock.unlock();
}
}
复制代码
在下面的代码当中加入咱们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。
if条件仍然满足,这个线程也需要从新获得锁,然而此时锁的状态是1,这个线程已经获得过一次锁了,然而自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,然而现在锁的状态是1,也就是说诚然这个线程获得过一次锁,然而它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。
可重入自旋锁思维
针对下面这种情况咱们需要实现一个可重入的自旋锁,咱们的思维大抵如下:
在咱们实现的自旋锁当中,咱们可能减少两个变量,owner一个用于存以后具备锁的线程,count一个记录以后线程进入锁的次数。
如果线程获得锁,owner = Thread.currentThread()并且count = 1。
当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则间接进行count++操作,而后就可能进入临界区了。
咱们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够间接将锁设置为0也就是未上锁的状态,这种情况间接进行count--操作,如果count等于1的话,说明线程以后的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其余线程能够获取锁。
可重入锁代码实现:
实现的可重入锁代码如下:
public class ReentrantSpinLock extends SpinLock {
private Thread owner;
private int count;
@Override
public void lock() {
if (owner == null || owner != Thread.currentThread()) { while (!value.compareAndSet(0, 1)); owner = Thread.currentThread(); count = 1;}else { count++;}
}
@Override
public void unlock() {
if (count == 1) { count = 0; value.compareAndSet(1, 0);}else count--;
}
}
复制代码
上面咱们通过一个递归程序去考据咱们写的可重入的自旋锁是否能够胜利工作。
测试程序:
import java.util.concurrent.TimeUnit;
public class ReentrantSpinLockTest {
public static int data;
public static ReentrantSpinLock lock = new ReentrantSpinLock();
public static void add(int state) throws InterruptedException {
TimeUnit.SECONDS.sleep(1);if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); lock.unlock();}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];for (int i = 0; i < 10; i++) { threads[i] = new Thread(new Thread(() -> { try { ReentrantSpinLockTest.add(1); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)));}for (int i = 0; i < 10; i++) { threads[i].start();}for (int i = 0; i < 10; i++) { threads[i].join();}System.out.println(data);
}
}
复制代码
下面程序的输入:
Thread-3 进入临界区 state = 1
Thread-3 进入临界区 state = 2
Thread-3 进入临界区 state = 3
Thread-0 进入临界区 state = 1
Thread-0 进入临界区 state = 2
Thread-0 进入临界区 state = 3
Thread-9 进入临界区 state = 1
Thread-9 进入临界区 state = 2
Thread-9 进入临界区 state = 3
Thread-4 进入临界区 state = 1
Thread-4 进入临界区 state = 2
Thread-4 进入临界区 state = 3
Thread-7 进入临界区 state = 1
Thread-7 进入临界区 state = 2
Thread-7 进入临界区 state = 3
Thread-8 进入临界区 state = 1
Thread-8 进入临界区 state = 2
Thread-8 进入临界区 state = 3
Thread-5 进入临界区 state = 1
Thread-5 进入临界区 state = 2
Thread-5 进入临界区 state = 3
Thread-2 进入临界区 state = 1
Thread-2 进入临界区 state = 2
Thread-2 进入临界区 state = 3
Thread-6 进入临界区 state = 1
Thread-6 进入临界区 state = 2
Thread-6 进入临界区 state = 3
Thread-1 进入临界区 state = 1
Thread-1 进入临界区 state = 2
Thread-1 进入临界区 state = 3
300
复制代码
从下面的输入后果咱们就可能知道,当一个线程能够获取锁的时候他能够进行重入,而且最终输入的后果也是正确的,因此考据了咱们写了可重入自旋锁是无效的!
总结
在本篇文章当中次要给大家介绍了自旋锁和可重入自旋锁的原理,并且实现了一遍,其实代码还是比较简略要害需要大家将这其中的逻辑理明显:
所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环。
可重入的含意就是一个线程已经竞争到了一个锁,在竞争到这个锁之后又一次有重入临界区代码的需要,如果能够保障这个线程能够从新进入临界区,这就叫可重入。
咱们在实现自旋锁的时候使用的是AtomicInteger类,并且咱们使用0和1这两个数值用于示意无锁和锁被占用两个状态,在获取锁的时候使用while循环不断进行CAS操作,直到操作胜利返回true,在开释锁的时候使用CAS将锁的状态从1变成0。
实现可重入锁最重要的一点就是需要记录是那个线程获得了锁,同时还需要记录获取了几次锁,因为咱们在解锁的时候需要进行判断,之后count = 1的情况才能将锁的状态从1设置成0。