两种方式实现自己的可重入锁

Posted qmlingxin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了两种方式实现自己的可重入锁相关的知识,希望对你有一定的参考价值。

本篇文章将介绍两种自己动手实现可重入锁的方法。

我们都知道JDK中提供了一个类ReentrantLock,利用这个类我们可以实现一个可重入锁,这种锁相对于synchronized来说是一种轻量级锁。

重入锁的概念

重入锁实际上指的就是一个线程在没有释放锁的情况下,可以多次进入加锁的代码块。

    public void a() {
        lock2.lock();
        System.out.println("a");
        b();
        lock2.unlock();
    }

    public void b() {
        lock2.lock();
        System.out.println("b");
        lock2.unlock();
    }
    new Thread(() -> {
        m.a();
    }).start();

这种情况下,如果我们加的锁不是支持可重入的锁,那么b方法中的代码块不会执行,如果我们的锁是一个重入锁,那么b方法中的打印代码块也会被执行。

土方法实现重入锁

首先我们先实现一个没有实现可重入的锁,这个锁实现接口Lock,代码如下:

public class MyLock implements Lock {

    //锁标记
    private boolean isLocked = false;

    @Override
    public synchronized void lock() {
        //如果已经有一个线程获得了锁,那么线程就一直等待
        while (isLocked)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        isLocked = true;
    }

    @Override
    public synchronized void unlock() {
        //可以进入的一定使已经获得锁的线程,那么直接改变标志,唤醒其他等待的线程
        isLocked = false;
        notify();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }


    @Override
    public Condition newCondition() {
        return null;
    }
}

代码中使用sychronized关键字,来控制只有一个线程可以获得锁。

由于sychronized关键字加在类的方法上的时候,内置锁对象实际使当前类的对象,因此,我们需要使用同一个对象来调用加锁、解锁方法。

这样我们就可以保证只有一个线程可以进入加解锁方法内部,然后通过isLocked来标记是否已经有线程获得了锁。

当我们使用上面介绍重入锁的测试方式来测验代码时,只会打印出a,之后将一直等待,无法打印b。

下面,我们将修改方法,让其实现可重入。

实际上,所谓可重入的方法就是将获得锁的线程记录下来,如果进入方法的线程可获得锁的线程是同一个线程,那么我们就可以直接获得锁,不需要等待。

实现方法如下:

    private boolean isLocked = false;
    //记录获得锁的线程
    private Thread lockBy = null;
    //记录获得锁的线程的重入次数
    private int count = 0;


    @Override
    public synchronized void lock() {
        //获取当前线程
        Thread currentThread = Thread.currentThread();
        //已经有线程获得锁,并且获得锁的线程不是当前线程,那么不满足获得锁,线程需要等待
        while (isLocked && currentThread != lockBy)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        //没有线程获得锁,或者获得锁的线程就是当前线程
        isLocked = true;
        lockBy = currentThread;
        //记录当前线程的重入次数
        count++;
    }

    @Override
    public synchronized void unlock() {
        //释放锁时,只有当获得锁的线程和当前线程是同一线程时才是正确的
        if (lockBy == Thread.currentThread()) {
            //线程重入次数减一
            count--;
            //只有当count变为0也就是所有获取锁的地方都已经释放了,才能够真正释放锁,修改标志位,唤醒其他线程
            if (count == 0) {
                notify();
                isLocked = false;
            }
        }
    }

当我们使用上面介绍重入锁的测试方式来测验代码时,将会打印出a、b,因为锁是可以重入的,不会出现一直等待的情况。

使用AQS类实现重入锁

AQS类简单介绍

AbstractQueuedSynchronizer类时JDK在1.5版本开始提供的一个可以用来实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)框架,在这里我们不关注它的其他功能,重点介绍一下如何利用AQS实现阻塞锁。

当我们要使用AQS实现一个锁时,我们需要在我们的锁类的内部声明一个非公共的内部帮助类,让这个类集成AbstractQueuedSynchronizer类,并实现其某些方法。
利用AQS类,我们可以实现两种模式的锁,一种是独占锁,一种是共享锁。这两种锁的帮助类需要实现的方法是不同,如果是独占锁,那么需要实现tryAcquire(int)tryRelease(int)方法;如果是共享锁,那么需要实现tryAcquireShared(int)tryReleaseShared(int)方法。

AQS类内部维护了一个FIFO的双向链表,用来保存所有争夺锁的线程,AQS源码中的Node类就是双向链表中节点的数据结构。

当使用AQS类加锁时,会调用方法acquire(int)方法中会调用,而acquire(int)方法中会调用tryAcquire(int)来尝试获得锁,如果获得锁成功,方法结束,如果获得锁失败,那么需要将线程维护到FIFO链表中,并且让新增加的线程进入等待状态,并且维护链表中线程的状态。(这部分代码比较复杂,可以参考网上对AQS的讲解,之后会再写一篇专门介绍AQS类的源码解析)

注:这个方法是忽略中断的,不忽略中断的方法,这里不做介绍

 public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
 }

当使用AQS释放锁时,会调用方法release(int)方法中会调用,而release(int)方法中会调用tryRelease(int)来尝试释放锁,如果释放锁成功后,如果FIFO链表中有线程,那么,会唤醒所有等待状态的线程。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

利用AQS实现可重入锁

实际上利用AQS实现一个可重入锁是非常容易的,首先给出代码。

实际上利用AQS实现锁和用土方法实现锁的思路大体上是相同的,只是,我们不需要关注线程的唤醒和等待,这些会有AQS帮助我们实现,我们只需要实现方法tryAcquire(int)tryRelease(int)就可以了。

这里我们实际上是应用AQS中的int值保存当前线程的重入次数。

加锁思路:

如果第一个线程进入可以拿到锁,可以返回true,

如果第二个线程进入,拿不到锁,返回false,

有一种特例(实现可重入),如果当前进入线程和当前保存线程为同一个,允许拿到锁,但是有代价,更新状态值,也就是记录线程的重入次数

public class MyLock2 implements Lock {
    private Helper helper = new Helper();
    //实现一个私有的帮助类,继承AQS类
    private class Helper extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
        
            //AQS中的int值,当没有线程获得锁时为0
            int state = getState();
            Thread t = Thread.currentThread();
            //第一个线程进入
            if (state == 0) {
                //由于可能有多个线程同时进入这里,所以需要使用CAS操作保证原子性,这里不会出现线程安全性问题
                if (compareAndSetState(0, 1)) {
                    //设置获得独占锁的线程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if (getExclusiveOwnerThread() == t) {
                //已经获得锁的线程和当前线程是同一个,那么state加一,由于不会有多个线程同时进入这段代码块,所以没有线程安全性问题,可以直接使用setState方法
                setState(state + 1);
                return true;
            }
            //其他情况均无法获得锁
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {

            //锁的获取和释放使一一对应的,那么调用此方法的一定是当前线程,如果不是,抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException();
            }
            
            int state = getState() - arg;

            boolean flag = false;
            
            //如果state减一后的值为0了,那么表示线程重入次数已经降低为0,可以释放锁了。
            if (state == 0) {
                setExclusiveOwnerThread(null);
                flag = true;
            }
            
            //无论是否释放锁,都需要更改state的值
            setState(state);
            
            //只有state的值为0了,才真正释放了锁,返回true
            return flag;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    @Override
    public void lock() {
        helper.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        helper.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        helper.release(1);
    }

    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }
}

至此,我们已经使用两种方式实现了一个重入锁。


以上是关于两种方式实现自己的可重入锁的主要内容,如果未能解决你的问题,请参考以下文章

重入锁

6.23Java多线程可重入锁实现原理

java可重入锁(ReentrantLock)的实现原理

Java并发包4--可重入锁ReentrantLock的实现原理

多线程之synchronoized实现可重入锁

Java并发程序设计(12)并发锁之可重入锁ReentrantLock