乐趣区

关于java:极客专栏打包实时更新内附资料文档

极客专栏打包 - 实时更新内附材料文档

下载地址:百度网盘
从零开始自己动手写自旋锁
咱们在写并发程序的时候,一个非常常见的需要就是保障在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保障一个时刻只有一个线程执行临界区的代码的方法就是锁🔒。在本篇文章当中咱们将会认真分析和学习自旋锁,所谓自旋锁就是通过 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); // 将数据初始化位 0
Thread 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。

退出移动版