开篇闲扯

后面几篇写了无关Java对象的内存布局、Java的内存模型、多线程锁的分类、Synchronized、Volatile、以及并发场景下呈现问题的三大罪魁祸首。看起来写了五篇文章,实际上也仅仅是写了个皮毛,用来应酬应酬局部公司“八股文”式的面试还行,然而在真正的在理论开发中会遇到各种稀奇古怪的问题。这时候就要通过线上的一些监测伎俩,获取零碎的运行日志进行剖析后再隔靴搔痒,比方JDK的jstack、jmap、命令行工具vmstat、JMeter等等,肯定要在正当的剖析根底上优化,否则可能就是零碎小“感冒”,后果做了个阑尾炎手术。

又扯远了,老样子,还是先说一下本文次要讲点啥,而后再一点点解释。本文次要讲并发包JUC中的三个类:ReentrantLock、ReentrantReadWriteLock和StampedLock以及AQS(AbstractQueuedSynchronizer)的一些基本概念。

先来个脑图:

Lock接口

public interface Lock {    //加锁操作,加锁失败就进入阻塞状态并期待锁开释    void lock();    //与lock()办法始终,只是该办法容许阻塞的线程中断        void lockInterruptibly() throws InterruptedException;    //非阻塞获取锁    boolean tryLock();    //带参数的非阻塞获取锁    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    //对立的解锁办法    void unlock();}

下面的源码展现了作为顶层接口Lock定义的一些根底办法。

lock只是个显示的加锁接口,对应不同的实现类,能够供开发人员进行自定义扩大。比方一些定时的可轮询的获取锁模式,偏心锁与非偏心锁,读写锁,以及可重入锁等,都可能很轻松的实现。Lock的锁是基于Java代码实现的,加解锁都是通过lock()和unlock()办法实现的。从性能上来说,Synchronized的性能(吞吐量)以及稳定性是略差于Lock锁的。然而,在Doug Lee参加编写的《Java并发编程实际》一书中又特别强调了,如果不是对Lock锁中提供的高级个性有相对的依赖,倡议还是应用Synchronized来作为并发同步的工具。因为它更简洁易用,不会因为在应用Lock接口时遗记在Finally中解锁而出bug。说到底,还是为了升高编程门槛,让Java语言更加好用。

其实常见的几个实现类有:ReentrantLock、ReentrantReadWriteLock、StampedLock
接下来将具体解说一下。

ReentrantLock

先简略举个应用的例子:

/** * FileName: TestLock * Author:   RollerRunning * Date:     2020/12/7 9:34 PM * Description: */public class TestLock {    private static int count=0;    private static Lock lock=new ReentrantLock();    public static void add(){        // 加锁        lock.lock();        try {            count++;            Thread.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }finally{            //在finally中解锁,加解锁必须成对呈现            lock.unlock();        }    }}

ReentrantLock只反对独占式的获取偏心锁或者是非偏心锁(都是基于Sync外部类实现,而Sync又继承自AQS),在它的外部类Sync继承了AbstractQueuedSynchronizer,并同时实现了tryAcquire()、tryRelease()和isHeldExclusively()办法等。同时,在ReentrantLock中还有其余两个外部类,一个是实现了偏心锁一个实现了非偏心锁,上面是ReentrantLock的局部源码:

/** * 非偏心锁 */static final class NonfairSync extends Sync {    private static final long serialVersionUID = 7316153563782823691L;    /**     * Performs lock.  Try immediate barge, backing up to normal     * acquire on failure.     */    final void lock() {        if (compareAndSetState(0, 1))            setExclusiveOwnerThread(Thread.currentThread());        else            acquire(1);    }    protected final boolean tryAcquire(int acquires) {        return nonfairTryAcquire(acquires);    }}/** * 偏心锁 */static final class FairSync extends Sync {    private static final long serialVersionUID = -3000897897090466540L;    //加锁时调用    final void lock() {        acquire(1);    }    /**     * Fair version of tryAcquire.  Don't grant access unless     * recursive call or no waiters or is first.     */    protected final boolean tryAcquire(int acquires) {        //获取以后线程        final Thread current = Thread.currentThread();        //获取父类 AQS 中的int型state        int c = getState();        //判断锁是否被占用        if (c == 0) {            //这个if判断中,先判断队列是否为空,如果为空则阐明锁能够失常获取,而后进行CAS操作并批改state标记位的信息            if (!hasQueuedPredecessors() &&                compareAndSetState(0, acquires)) {                //CAS操作胜利,设置AQS中变量exclusiveOwnerThread的值为以后线程,示意获取锁胜利                setExclusiveOwnerThread(current);                //返回获取锁胜利                return true;            }        }        //而当state的值不为0时,阐明锁曾经被拿走了,此时判断锁是不是本人拿走的,因为他是个可重入锁。        else if (current == getExclusiveOwnerThread()) {            //如果是以后线程在占用锁,则再次获取锁,并批改state的值            int nextc = c + acquires;            if (nextc < 0)                throw new Error("Maximum lock count exceeded");            setState(nextc);            return true;        }        //当标记位不为0,且占用锁的线程也不是本人时,返回获取锁失败        return false;    }}/** * AQS中排队的办法 */final boolean acquireQueued(final Node node, int arg) {    boolean failed = true;    try {        boolean interrupted = false;        for (;;) {            final Node p = node.predecessor();            if (p == head && tryAcquire(arg)) {                setHead(node);                p.next = null; // help GC                failed = false;                return interrupted;            }            if (shouldParkAfterFailedAcquire(p, node) &&                parkAndCheckInterrupt())                interrupted = true;        }    } finally {        if (failed)            cancelAcquire(node);    }}

下面是以偏心锁为例对源码进行了简略的正文,能够依据这个思路,看一看非偏心锁的源码实现,再敞开源码试着画一下整个流程图,理解其外部实现的真谛。我先画为敬了:

这里涵盖了ReentrantLock的加锁根本流程,观众老爷是不是能够试着画一下解锁的流程,还有就是这个例子是独占式偏心锁,独占式非偏心锁的总体流程大差不差,这里就不赘述了。

ReentrantReadWriteLock

一个简略的应用示例,大家能够本人运行感受一下:

/** * FileName: ReentrantReadWriteLockTest * Author:   RollerRunning * Date:     2020/12/8 6:48 PM * Description: ReentrantReadWriteLock的简略应用示例 */public class ReentrantReadWriteLockTest {    private static ReentrantReadWriteLock READWRITELOCK = new ReentrantReadWriteLock();    //取得读锁    private static ReentrantReadWriteLock.ReadLock READLOCK = READWRITELOCK.readLock();    //取得写锁    private static ReentrantReadWriteLock.WriteLock WRITELOCK = READWRITELOCK.writeLock();    public static void main(String[] args) {        ReentrantReadWriteLockTest lock = new ReentrantReadWriteLockTest();        //别离启动两个读线程和一个写线程        Thread readThread1 = new Thread(new Runnable() {            @Override            public void run() {                lock.read();            }        },"read1");        Thread readThread2 = new Thread(new Runnable() {            @Override            public void run() {                lock.read();            }        },"read2");        Thread writeThread = new Thread(new Runnable() {            @Override            public void run() {                lock.write();            }        },"write");        readThread1.start();        readThread2.start();        writeThread.start();    }    public void read() {        READLOCK.lock();        try {            System.out.println("线程 " + Thread.currentThread().getName() + " 获取读锁。。。");            Thread.sleep(2000);            System.out.println("线程 " + Thread.currentThread().getName() + " 开释读锁。。。");        } catch (Exception e) {            e.printStackTrace();        } finally {            READLOCK.unlock();        }    }    public void write() {        WRITELOCK.lock();        try {            System.out.println("线程 " + Thread.currentThread().getName() + " 获取写锁。。。");            Thread.sleep(2000);            System.out.println("线程 " + Thread.currentThread().getName() + " 开释写锁。。。");        } catch (Exception e) {            e.printStackTrace();        } finally {            WRITELOCK.unlock();        }    }}

后面说了ReentrantLock是一个独占锁,即不管线程对数据执行读还是写操作,同一时刻只容许一个线程持有锁。然而在一些读多写少的场景下,这种不分青红皂白就无脑加锁对的做法不够极客也很影响效率。因而,基于ReentrantLock优化而来的ReentrantReadWriteLock就呈现了。这种锁的思维是“读写锁拆散”,多个线程能够同时持有读锁,然而不容许多个线程持有雷同写锁或者同时持有读写锁。要害源码解读:

//加共享锁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);    //这里做了三个判断:是否阻塞即是否为偏心锁、持有该共享锁的线程是否超过最大值、CAS加共享读锁是否胜利    if (!readerShouldBlock() &&        r < MAX_COUNT &&        compareAndSetState(c, c + SHARED_UNIT)) {        //以后线程为第一个加读锁的,并设置持有锁线程数量        if (r == 0) {            firstReader = current;            firstReaderHoldCount = 1;        } else if (firstReader == current) {            //以后示意为重入锁            firstReaderHoldCount++;        } else {            HoldCounter rh = cachedHoldCounter;            if (rh == null || rh.tid != getThreadId(current))                //获取以后线程的计数器                cachedHoldCounter = rh = readHolds.get();            else if (rh.count == 0)                //增加到readHolds中,这里是基于ThreadLocal实现的,每个线程都有本人的readHolds用于记录本人重入的次数                readHolds.set(rh);            rh.count++;        }        return 1;    }    return fullTryAcquireShared(current);}final int fullTryAcquireShared(Thread current) {    HoldCounter rh = null;    for (;;) {        int c = getState();        if (exclusiveCount(c) != 0) {            if (getExclusiveOwnerThread() != current)                return -1;            // else we hold the exclusive lock; blocking here            // would cause deadlock.        } else if (readerShouldBlock()) {            // Make sure we're not acquiring read lock reentrantly            if (firstReader == current) {                // assert firstReaderHoldCount > 0;            } else {                if (rh == null) {                    rh = cachedHoldCounter;                    if (rh == null || rh.tid != getThreadId(current)) {                        rh = readHolds.get();                        if (rh.count == 0)                            readHolds.remove();                    }                }                if (rh.count == 0)                    return -1;            }        }        if (sharedCount(c) == MAX_COUNT)            throw new Error("Maximum lock count exceeded");        if (compareAndSetState(c, c + SHARED_UNIT)) {            if (sharedCount(c) == 0) {                firstReader = current;                firstReaderHoldCount = 1;            } else if (firstReader == current) {                firstReaderHoldCount++;            } else {                if (rh == null)                    rh = cachedHoldCounter;                if (rh == null || rh.tid != getThreadId(current))                    rh = readHolds.get();                else if (rh.count == 0)                    readHolds.set(rh);                rh.count++;                cachedHoldCounter = rh; // cache for release            }            return 1;        }    }}

在ReentrantReadWriteLock中,也是基于AQS来实现的,在它的外部应用了一个int型(4字节32位)的stat来示意读写锁,其中高16位示意读锁,低16位示意写锁,而对于读写锁的判断通常是对int值以及高下16位进行判断。接下来用一张图展现一下获取共享的读锁过程:

至此,别离展现了获取ReentrantLock独占锁ReentrantReadWriteLock共享读锁的过程,心愿可能帮忙大家跟面试官PK。

总结一下后面说的两种锁:

当线程持有读锁时,那么就不能再获取写锁。当A线程在获取写锁的时候,如果以后读锁被占用,立刻返回失败失败。

当线程持有写锁时,该线程是能够持续获取读锁的。当A线程获取读锁时如果发现写锁被占用,判断以后写锁持有者是不是本人,如果是本人就能够持续获取读锁,否则返回失败。

StampedLock

StampedLock其实是对ReentrantReadWriteLock进行了进一步的降级,试想一下,当有很多读线程,然而只有一个写线程,最蹩脚的状况是写线程始终竞争不到锁,写线程就会始终处于期待状态,也就是线程饥饿问题。StampedLock的外部实现也是基于队列和state状态实现的,然而它引入了stamp(标记)的概念,因而在获取锁时会返回一个惟一标识stamp作为以后锁的版本,而在开释锁时,须要传递这个stamp作为标识来解锁。

从概念上来说StampedLock比RRW多引入了一种乐观锁的思维,从应用层面来说,加锁生成stamp,解锁须要传同样的stamp作为参数。
最初贴一张我整顿的这部分脑图:

最初,感激各位观众老爷,还请三连!!!
点赞关注不迷路,再次感激!