面试官:自己如何实现一个Java锁?

        在Java中现有的锁有很多,比如:synchronize 、ReentrantLock 、ReadWriteLockCountDownLatchSemaphone等等。

        如果让我们自己实现一个锁得需要用到什么知识?实现起来难不难呢?今天就让我们一起来尝试下吧!Go~

        首先,我们先来思考下,锁的作用是什么?锁是解决什么问题?

        简单来说就是防止竞条件的时候,产生错误结果。

        那我们先将锁简单化,先实现个简单的锁,多线程去处理同一块业务代码,但只能有一个线程可以拿到锁,其余的排队阻塞。如果让我们按自己思路去设计的话,可以猜想下,我们会碰到几个问题?

  1. 该怎么表示锁被占用了?被谁占用?

  2. 如何保证锁的争夺是原子性的?

  3. 抢不到锁的线程如何阻塞?阻塞后改如何唤醒呢?

  4. 抢不到锁的线程该怎么保存?

        第一个问题,我们完全可以用一个变量Thread owner用来存被占用的线程,如果该变量owner有值,说明锁被占用。不过这里有个小坑,这个变量需要时volatile修饰的,防止多线程之间无法获取最新的状态。        

        首先,我们得知道线程执行操作的时候,会把相应的数据从主内存中拷贝一份,在拷贝出来的副本上进行操作,操作完之后再刷新到主内存中去,这样就有个问题,多个线程同时读取同一个变量,对它进行修改,会操作操作被覆盖的情况。

面试官:自己如何实现一个Java锁?_第1张图片

 

        比如变量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 :

    • 不释放锁

    • 不要求执行顺序

  1. suspendresume 已经被废弃了,所以不考虑。

  2. wait/notify 需要用到synchronize,既然要用到synchronize标识了,我们还实现个锤子的锁,所以也不考虑。

  3. 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 呢?

  1. 其中一个原因是,我们需要让线程重新去争夺锁资源。
  2. 第二个原因是,线程的伪唤醒(spurious wakeup),对于LockSupport.park()来说就是除了unparkinterrupt之外的原因。

        引用下Stackoverflow网站中的网友回答:

  1. wait前会释放监视器,被唤醒后又要重新获取,这瞬间可能有其他线程刚好先获取到了监视器,从而导致状态发生了变化, 这时候用while循环来再判断一下条件(比如队列是否为空)来避免不必要或有问题的操作。 

  2. 这种机制还可以用来处理伪唤醒(spurious wakeup),所谓伪唤醒就是 no reason wakeup,对于 LockSupport.park() 来说就是除了 unpark 和 interrupt 之外的原因。

https://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again

        好了,我们现在已经成功的自己实现了锁,是不是很简单,其实用到的不过就是:

  • volatile 可见性        
  • CAS 机制保证原子性操作

  • 线程通信

        大家可以看完这篇文章后,再去看ReentrantLock 就会知道Java设计也是差不多,不过他们有更多细节上的处理。大家可以去自己尝试看下~

 

大家如果觉得文章不错的话,可以关注“架构日志”公众号哦~

 

你可能感兴趣的:(Java)