Java并发——ReentrantLock可重入锁、ReentrantReadWriteLock读写锁源码解析

版权声明:个人博客:blog.suyeq.com 欢迎访问 https://blog.csdn.net/hackersuye/article/details/84969311

    写在前面,阅读这篇文章,需要这些知识:
    Java并发——Thread类解析、线程初探
    Java并发——CAS原子操作
    Java并发——AQS框架详解

ReentrantLock

    ReentrantLock类是Java并发包里可重入锁的实现,同时它的大部分功能交由同步器AQS框架完成,它是AQS独占锁的实现。相比较于内置锁ReentrantLock拓展了内置锁的功能,在其功能上在增加了中断异常、轮询锁、定时锁等功能,因为ReentrantLock的实现是程序级别的,它的灵活性相较于内置锁要高。其绑定的Condition则大大扩展了其线程间协程通信的功能。
    ReentrantLock是完全实现了内置锁的功能的,它在程序级别上利用AQS并发框架CAS原子操作来实现可重入性、原子性以及互斥性。先来看看一个ReentrantLock的一个实现:

public class Test {
    public static void main (String args[]) throws InterruptedException {
        Lock lock=new ReentrantLock();
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                System.out.println("拉姆拿到锁啦");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
                System.out.println("拉姆释放锁啦");
            }
        });

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("蕾姆准备拿锁啦");
                lock.lock();
                System.out.println("蕾姆拿到锁啦");
                lock.unlock();
            }
        });

        thread.start();
        thread1.start();
    }
}
//输出:拉姆拿到锁啦
//蕾姆准备拿锁啦
//拉姆释放锁啦
//蕾姆拿到锁啦

    ReentrantLock是一种独占锁,也即是互斥锁,当一个线程持有锁时,另外一个线程必须陷入等待。同时ReentrantLock内置锁很大的一个区别是,内置锁在因为锁的竞争上陷入的等待是不可响应中断,而ReentrantLock提供了lockInterruptibly方法来响应这个中断,注意lock方法也不可响应

public class Test {
    public static void main (String args[]) throws InterruptedException {
        Lock lock=new ReentrantLock();
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("拉姆拿到锁啦");
                lock.unlock();
                System.out.println("拉姆释放锁啦");
            }
        });

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("蕾姆准备拿锁啦");
                lock.lock();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("蕾姆拿到锁啦");
                lock.unlock();
            }
        });

        thread1.start();
        thread.start();
        thread1.interrupt();
    }
}
//报java.lang.InterruptedException异常

    当蕾姆拿到锁陷入睡眠时,拉姆请求锁就会失败,这时候中断thread1就会抛出异常。追本溯源,找到源码实现如下:

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
            //如果线程被中断,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
           //没有中断但竞争资源失败,则进入同步队列
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

    其实现是由AQS框架实现的,当加锁的时候没有被中断时,AQS会把竞争失败的线程加入到同步队列中,doAcquireInterruptibly方法是一个响应中断的并不断与其它线程竞争资源的方法,如果在该方法运行时线程被中断,那么则会抛出中断异常。如果对AQS并发框架不了解的同学可以参照Java并发——AQS框架详解

    上文说的ReentrantLockAQS独占锁的实现,其内部的Sync继承了AQS框架,用于实现ReentrantLock里面公平锁和非公平锁的实现。关于公平锁,它的请求资源的获取如下:

final void lock() {
            acquire(1);
        }
        
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //首次获得锁,设置获得锁的线程独占
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //再次获得锁,将AQS中的state加acquires
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    由源码可以看出,当公平锁请求资源的时候,会首先判断该锁是否已经被其它线程占用了,即判定state是否为零,而hasQueuedPredecessors方法则是判断当前同步队列里是否有等待获取资源的线程,有则返回true,两个条件都满足时,则设置为当前线程独占,如果判定当前线程已经持有了该锁,那么会将state加上它再次请求获取的资源量,这也是可重入性的实现。当前面两者都失败后,那么根据AQS框架的acquire方法,它会把请求资源失败的线程加入到同步队列的队尾,等待前面的线程都运行完成后,该线程就会得到资源进行运行。也就是说,公平锁是按同步队列中的顺序来执行因抢不到资源进入同步队列中的线程,保证公平性,先来先运行,后来的后运行。关于非公平锁,它的请求资源方法是在Sync类里面实现的,它的请求资源的源码如下:


final void lock() {
			//直接进行抢断
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}
        
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

    从源码中看出,非公平锁在请求资源师是直接进行抢断的,还有非公平锁少了hasQueuedPredecessors方法,也就是说,不管同步队列里面有无线程,当锁释放的时候,它都会与其它线程竞争资源。而且当锁被释放时,新来的线程总能获得锁先执行。非公平锁就是当锁释放时,新来的线程可以与位于同步队列中的线程同时竞争资源(一般能成功),而不像公平锁那样先来的线运行。关于两者的释放锁,都是调用release方法:

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	//唤醒在等待队列中的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

    释放即减少对应的state变量。当state变量为0的时候则会判定当前线程释放锁,将独占该锁的线程置为null。如果要说ReentrantLock有什么重大缺陷的话,那么就是释放锁的时候,因为它不像内置锁那样完全交给JVM执行,需要靠调用者自己解除锁,而调用者很有可能会遗忘掉这件事。值得一提的是,ReentrantLock的默认构造方法是非公平锁,因为事实上非公平锁的性能要比公平锁的性能要好一些,非公平锁减少了线程从新获得资源的上下文切换:

public ReentrantLock() {
        sync = new NonfairSync();
    }

    而且,公平锁的tryLock方法也是非公平的获取资源:

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    轮询锁的应用是调用tryLock方法的,从源码中可以看出,每次tryLock底层用的是非公平锁的获取方式,获取资源成功则返回true,否则就返回false,利用这个特性,可以定义轮询锁:

while (true){
            if (lock.tryLock()){
                //执行要执行的操作
            }
        }

    而定时锁则是调用它的tryLock(long time, TimeUnit unit)方法,利用时间来等待,等待超时则视为获取失败,进行下一步操作。

ReentrantReadWriteLock

    ReentrantReadWriteLock中文名字叫做可重入读写锁,其内部含有两种锁,一种是读锁,另外一种是写锁。它规定了最多只有一个线程持有写锁,但是可以有多个线程持有读锁,而且当其中的写锁先被线程持有时,要等到写锁释放后才能让其它线程获取到读锁,反过来也是如此。从另外的角度来讲,ReentrantReadWriteLock的写锁是独占锁,而其读锁是共享锁,但是会写/读互斥,读/写互斥以及写/写互斥,与ReentrantLock一样,大部分的功能都是交由AQS框架完成的。如下示例:

public class Test {
    public static void main (String args[]) throws InterruptedException {
        ReadWriteLock lock=new ReentrantReadWriteLock();
        Lock write=lock.writeLock();
        Lock read=lock.readLock();
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                read.lock();
                System.out.println("拉姆拿到锁啦");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                read.unlock();
                System.out.println("拉姆释放锁啦");
            }
        });
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                read.lock();
                System.out.println("蕾姆拿到锁啦");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("蕾姆释放锁啦");
                read.unlock();
            }
        });
        thread.start();
        thread1.start();
    }
}
//输出:拉姆拿到锁啦
//蕾姆拿到锁啦
//拉姆释放锁啦
//蕾姆释放锁啦

    在同时是读锁的情况下,并没有因为锁而陷入阻塞状态,读者可以多次将上面的例子改成读写、写读等,就能得到上述的结论。与ReentrantLock一样,ReentrantReadWriteLock内部也定义了Sync抽象类来继承AQS并发框架,并重写里面的方法。它也分为两种锁,公平锁与非公平锁,其默认的是非公平锁:

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    ReentrantReadWriteLock将AQS框架表示资源状态的state分为两部分,也就是两个short类型的短整数,低位的一个表示独占(写锁)锁定保持计数,而高位的一个表示共享(读锁)锁保持计数,它的计算方式如下:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    这是什么意思呢?在AQSstate代表着锁的计数,为0的是代表锁没有被线程持有,当被线程持有时,state加1,再次被持有的线程重入时,state再次加1,上面的阐述表明stateReentrantReadWriteLock有最大值,而读锁的state表示着共享的线程,同理它也有着最大值。更深层次的,可以利用state来判断当前运行的是写锁还是读锁。看看写锁的请求方法:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            //判断是否有还有资源(state)大小来支持可重入
            int w = exclusiveCount(c);
            //代表有线程持有写锁
            if (c != 0) {
                // 如果state的低位为0,但是c不等于0,则表明有读锁在执行
                //或者不是当前线程持有
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            //如果是公平锁,那么新的线程在同步队列里有线程时应该加入到同步队列
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
              //设置当前线程独占
            setExclusiveOwnerThread(current);
            return true;
        }

    我们来分析一下这个方法,首先获取state的值,这个值我们可以利用它来判断当前线程持有的锁时读锁还是写锁,从而判定是否进行下一步。当state的值不为0,但是state的低位计算出来是0,代表写锁的计数是0,那么说明有线程持有了读锁,那么请求写锁的线程请求资源失败,进入同步队列。writerShouldBlock方法具体视公平锁与非公平锁而定,当是公平锁的时候,会判定同步队列中是否有线程在等待获取资源,会就会把新的请求线程加入到同步队列队尾中,而非公平锁则是默认不查找,读锁的readShouldBlock原理一样。再来看读锁的请求实现:

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //.....
                return 1;
            }
            return fullTryAcquireShared(current);
        }

    与独占锁不同的是,共享锁可以多个线程共享一把锁,当线程持有写锁时,请求读锁的线程都会进入同步队列中,当线程持有的是读锁时,那么新来的请求读锁的线程可以获取到锁。

    当是公平锁时,线程持有的是写锁,释放锁的时候,会按照同步队列中的顺序来执行请求读锁或者请求写锁的线程,当是非公平锁时,持有锁的线程释放锁时,这时候如果有请求读锁和请求写锁的线程,那么会先运行请求写锁的线程,保持数据的一致性。读写锁的应用场景适用于读的次数很多,但是写的次数很少的场景,这时候ReentrantReadWriteLock效率会很高。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/84969311