在Java中现有的锁有很多,比如:synchronize
、ReentrantLock
、ReadWriteLock
、CountDownLatch
、Semaphone
等等。
如果让我们自己实现一个锁得需要用到什么知识?实现起来难不难呢?今天就让我们一起来尝试下吧!Go~
首先,我们先来思考下,锁的作用是什么?锁是解决什么问题?
简单来说就是防止竞态条件的时候,产生错误结果。
那我们先将锁简单化,先实现个简单的锁,多线程去处理同一块业务代码,但只能有一个线程可以拿到锁,其余的排队阻塞。如果让我们按自己思路去设计的话,可以猜想下,我们会碰到几个问题?
该怎么表示锁被占用了?被谁占用?
如何保证锁的争夺是原子性的?
抢不到锁的线程如何阻塞?阻塞后改如何唤醒呢?
抢不到锁的线程该怎么保存?
第一个问题,我们完全可以用一个变量Thread owner
用来存被占用的线程,如果该变量owner
有值,说明锁被占用。不过这里有个小坑,这个变量需要时volatile
修饰的,防止多线程之间无法获取最新的状态。
首先,我们得知道线程执行操作的时候,会把相应的数据从主内存中拷贝一份,在拷贝出来的副本上进行操作,操作完之后再刷新到主内存中去,这样就有个问题,多个线程同时读取同一个变量,对它进行修改,会操作操作被覆盖的情况。
比如变量i=0;
线程A、B都读取到了值i=0;
,然后线程A先执行 i++;
,再把变量i
的最新值刷新到主内存中去,此时主内存中的i=1;
,这时,线程B也执行了i++;
,也得到i=1
的情况,再刷新到主内存去,主内存中的变量i=1
,这样就会导致变量i
的值少了1。
volatile
关键字就是为了解决线程之间变量可见性的,让线程B能够实时知道变量i
的值已经被线程A发生改变,让线程B重新去主内存读取变量i
最新的值。
第二个问题,我们可以直接采用CAS
机制来保证操作的原子性问题。
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 true。否则,返回 false。
第三个问题,我们知道线程的通信方式有:
suspend和resume:
JDK 已废弃
不释放锁
要求执行顺序
wait/notify/notifyAll:
要求执行顺序
释放锁
park/unpark :
不释放锁
不要求执行顺序
suspend和resume 已经被废弃了,所以不考虑。
wait/notify 需要用到synchronize,既然要用到synchronize标识了,我们还实现个锤子的锁,所以也不考虑。
park/unpark 不释放锁,没问题,我们本来就没用到Synchronize,无所谓。不要求park/unpark执行顺序,那更好了,这样还可以防止死锁问题。所以我们就选它了!
第四个问题,排队阻塞的线程就暂时用集合将其存起来,等释放锁后,再将它们用unpark
唤醒。
现在让我们来看下具体的代码实现吧:
import org.apache.commons.collections.CollectionUtils;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
/**
* 简单锁实现
*/
public class SimpleLock {
// 变量owner存储占用锁的线程
private volatile AtomicReference owner = new AtomicReference();
// 阻塞队列
private volatile LinkedBlockingQueue waiters = new LinkedBlockingQueue<>();
// 争夺锁方法
public void lock() {
while(!tryLock()) { // ①:这里为什么要用while?而不是用if呢?
// 无法获取到锁,添加到阻塞等待队列
waiters.add(Thread.currentThread());
LockSupport.park();
}
// 如果拿到锁了,就将其移除阻塞等待队列
waiters.remove(Thread.currentThread());
}
// 尝试获取锁的方法
public boolean tryLock() {
// CAS机制拿当前线程跟主内存中的值对比,owner是否为null,如果是就将其值设置为当前线程
return owner.compareAndSet(null, Thread.currentThread());
}
// 释放锁
public void unlock() {
// CAS机制拿当前线程跟主内存中的值对比,是否是同一个线程,如果是就将其值设置为null
if (owner.compareAndSet(Thread.currentThread(), null)) {
// 将所有阻塞等待队列的线程都唤醒,让他们去抢锁。(非公平)
if (CollectionUtils.isNotEmpty(waiters)) {
waiters.stream().forEach(waiter -> {
LockSupport.unpark(waiter);
});
}
}
}
}
测试用例:
public class LockTest {
private int i = 0;
SimpleLock myLock = new SimpleLock();
private void add() {
myLock.lock();
try {
i++;
} finally {
myLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
for (int j = 0; j < 4; j++) {
new Thread(() -> {
for (int k = 0; k < 1000; k++) {
lockTest.add();
}
}).start();
}
Thread.sleep(2000);
System.out.println("结果:" + lockTest.i);
}
}
输出结果:
结果:4000
注:①. 这里为什么要用 while
?而不是用 if
呢?
LockSupport.park()
来说就是除了unpark
和interrupt
之外的原因。引用下Stackoverflow网站中的网友回答:
wait前会释放监视器,被唤醒后又要重新获取,这瞬间可能有其他线程刚好先获取到了监视器,从而导致状态发生了变化, 这时候用while循环来再判断一下条件(比如队列是否为空)来避免不必要或有问题的操作。
这种机制还可以用来处理伪唤醒(spurious wakeup),所谓伪唤醒就是 no reason wakeup,对于 LockSupport.park() 来说就是除了 unpark 和 interrupt 之外的原因。
https://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again
好了,我们现在已经成功的自己实现了锁,是不是很简单,其实用到的不过就是:
CAS 机制保证原子性操作
线程通信
大家可以看完这篇文章后,再去看ReentrantLock
就会知道Java设计也是差不多,不过他们有更多细节上的处理。大家可以去自己尝试看下~
大家如果觉得文章不错的话,可以关注“架构日志”公众号哦~