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


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

    我们都知道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();
        }
    }
    

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

  • 相关阅读:
    linux 回收站 路径
    Linux 让进程在后台可靠运行的几种方法
    用marquee和div+js实现首尾相连循环滚动效果
    轻型数据库SQLite结合PHP的开发
    linux系统权限修复——学生误操作!
    2009级 毕业设计 题目
    linux下硬盘uuid查看及修改设置
    创建网站地图
    用上下左右箭头键在textbox中的光标跳转
    SHELL中时间的比较
  • 原文地址:https://www.cnblogs.com/qmlingxin/p/9222305.html
Copyright © 2020-2023  润新知