JDK读写锁原理

ReentrantReadWriteLock

干啥的?

可重入的读写锁

读写锁是干啥的?有什么用?和互斥锁有何区别?为什么会有读写锁的存在?它解决了什么问题?它是如何实现的?

将原来的互斥锁,切割为了两把锁:读锁+写锁

为什么要切割?

考虑一下,三个线程:A、B、C,其中线程A和B为读取数据的线程,而C为修改数据的线程,且C很少去修改数据,此时为什么要让A和B争用互斥锁呢?因为此时它俩并不修改数据,所以同时并发读取是没有任何问题的,所以我们把锁切割为了两把,当读时,获取读锁,写时获取写锁,且读锁可以多个线程同时获取,此时我们称读锁为共享锁,写锁为互斥锁。

我们自己实现一个读写锁

问题一:如果两个线程同时去拿读锁和写锁,因为两个判断不是原子的,所以都有可能进去if判断,那么两个线程就分别拿到了读写锁。所以我们优化了程序,添加了第三个变量。虽然保障了原子性,但是读锁变为了互斥锁。

问题二:读锁不能共享。

int state;

32位的变量,此时我们考虑切割为高16位+低16位,分别用于表示读锁和写锁,此时由于只需要操作一个变量,所以只需要一次CAS即可,不需要保证多个操作的原子性。

怎么解决写锁饥饿问题?

在获取读锁之前,判断当前有没有写锁排队即可

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
}

为什么读写锁还有公平锁和非公平锁的实现?

这里的公平和非公平是对写锁而言的。

如果一个线程已经持有了读锁,那么此时再来一个写锁,就会放到等待队列,后面再来了读锁就会继续往后排队,此时又来了一个写锁,如果是公平的,它会看一下前面有没有写锁在排队,如果是非公平的,它会直接试着加写锁。

为什么非公平锁性能高于公平锁?

线程上下文切换时间+调度延迟时间

public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
}
public static class WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
}

读锁和写锁都是拿的同一个同步器(Sync),这个同步器继承了AQS类,实现了大部分的逻辑。读共享,写互斥都由它来保证。

如果我想要知道当前线程获取了多少次共享锁,也即重入了多少次共享锁怎么办?因为state的高16位是所有读线程共享的。通过ThreadLocal来记录每个线程获取了多少次共享锁即可,所以我们称state的高16位用于存储所有读线程获取共享锁的次数,TL用于表示当前线程自己的重入次数,sum(all thread tl count)= state >>16

假如所有时间都是同一个线程获取读锁,那么有没有必要使用TL?因为TL占用内存,没必要,所以我们做一个优化,在读写锁中维护一个:firstCount,保存第一个线程对象和获取锁的数量即可

abstract static class Sync extends AbstractQueuedSynchronizer {
    //最后一个线程获取锁的次数,最后一个线程有可能成为第一个
    private transient HoldCounter cachedHoldCounter;

    private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;
    Sync() {
        readHolds = new ThreadLocalHoldCounter();
        //获取一个volatile的变量,保证之前CPU缓存的值刷到主存
        setState(getState()); // ensures visibility of readHolds
    }
    protected final int tryAcquireShared(int unused) {
        Thread current = Thread.currentThread();
        int c = getState();
        //当前写锁的count不为0,并且还不是当前线程持有的写锁,解决线程饥饿问题
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;
        int r = sharedCount(c);
        //读锁应不应该阻塞,如果是非公平锁,那么就会看队列中是否有写锁在排队
        if (!readerShouldBlock() &&
            //判断r如果小于MAX_COUNT,那么读锁的count++,表示获取到了读锁
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) {
            //如果r=0,表示当前线程是第一个获取读锁的线程
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                //如果r!=0,判断当前线程是不是第一个
                firstReaderHoldCount++;
            } else {
                //不是第一个,那么就获取最后一个线程
                HoldCounter rh = cachedHoldCounter;
                //判断当前线程是不是最后一个线程,如果不是,那么就申请一个ThreadLocal,然后++
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        //上面这些前置判断都是对这么方法的优化,有很多人走到这里,直接懵逼,不知道这是啥情况
        //经验:在看道格李写的代码时,请注意,经常做优化,就是把一些常见的场景前置,保障性能
        return fullTryAcquireShared(current);
    }
}
第21~24行的三个判断如果是false,下面对应着处理代码
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
//道格李的一贯做法,如果多线程下无锁,必定会使用死循环
for (;;) {
    int c = getState();
    //已经有线程持有互斥锁,并且还不是当前线程
    if (exclusiveCount(c) != 0) {
        if (getExclusiveOwnerThread() != current)
            return -1;
    } 
    //第21~24行的三个判断如果是false,下面对应着处理代码
    else if (readerShouldBlock()) {//读锁应该被阻塞
        if (firstReader == current) {
            //如果当前线程就是第一个读线程,那么啥也不做
        } else {
            //如果不是第一个,那么就对线程的ThreadLocal变量进行操作
            if (rh == null) {
                rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current)) {
                    rh = readHolds.get();
                    if (rh.count == 0)
                        readHolds.remove();
                }
            }
            //表明没有获取读锁,返回-1,阻塞当前线程
            if (rh.count == 0)
                return -1;
        }
    }
    if (sharedCount(c) == MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    //和tryAcquireShared中一样
    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;
    }
}
}

进来的时候判断,如果有互斥锁,那么直接退出,没有的话,判断该线程要不要阻塞,如果需要阻塞,那么清除一下它的ThreadLocal,如果不需要阻塞,先对state进行+1,然后对自身的count进行++,加的时候分为几种情况,如果是第一个线程,那么直接firstReaderHoldCount++,如果不是,那么对ThreadLocal中的变量进行++

static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
        return false; 
    }
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
if (firstReader == current) {
    // assert firstReaderHoldCount > 0;
    if (firstReaderHoldCount == 1)
        firstReader = null;
    else
        firstReaderHoldCount--;
} else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
        rh = readHolds.get();
    int count = rh.count;
    if (count <= 1) {
        readHolds.remove();
        if (count <= 0)
            throw unmatchedUnlockException();
    }
    --rh.count;
}
for (;;) {
    int c = getState();
    int nextc = c - SHARED_UNIT;
    if (compareAndSetState(c, nextc))
        //读锁释放完毕之后,看看等待队列中有没有排队的线程,如果有就唤醒
        return nextc == 0;
}
}

public static class WriteLock implements Lock, java.io.Serializable {
    public void unlock() {
        sync.release(1);
    }
    //Sync的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) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }
}
static final class FairSync extends Sync {
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

AQS

如何保证三个操作的原子性?

可以加锁,但是这样会导致性能严重下降,那么此时只能通过CAS操作来保证多线程操作的安全性,但是CAS只能保证一个操作的安全,那么这三个操作应该先保证哪个呢?

特别注意,当上面CAS成功后,有一瞬间,这里的pred.next并没有关联,会导致什么问题?

有一瞬间,你通过head进行遍历的时候,是到达不了最后一个节点的!

如何获取最新的节点呢?

通过tail指针往前遍历即可。

addWaiter方法使用优化前置,把enq里的代码移到了前面

static final class Node {
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                //特别注意,当上面CAS成功后,有一瞬间,这里的pred.next并没有关联
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
}

当加入阻塞队列后,调用该方法考虑是否将当前线程进行阻塞,在看该方法时,请考虑一个情况:

假如在添加到阻塞队列后,当前状态时无锁时,怎么办?那么一定是尝试获取锁。

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);
    }
}
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //如果前驱节点的状态是SIGNAL,那么可以安心睡眠,因为SIGNAL状态代表了上一个线程是
            //活的,它可以通知你
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || //为什么这里有可能为空?因为我们首先更新的是tail引用,然后才是
            //t.next = node; 有可能一瞬间为空
            s.waitStatus > 0 //后继节点居然是无效节点?因为上面第32行更新的时候不是原子的,
            //所以有可能一瞬间指向的仍然是无效节点
            ) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

共享锁获取锁

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
    //获取锁失败,进入此方法,获取锁的逻辑由子类自己来实现
    doAcquireShared(arg);
}
    private void doAcquireShared(int arg) {
        //和互斥锁类似,只不过这里添加的是共享锁节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //如果获取锁成功,尝试唤醒后面的共享节点,因为共享锁是可以多线程同时
                        //获取
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

此时有一个场景:

Semaphore s = new Semaphore(2);

线程A,B去acquire,此时AB都获取到了信号量,再来两个线程C,D此时就会去队列中排队。

当A调用了release释放了信号量,此时唤醒了C,C执行完tryAcquireShared(arg);(上面第15行代码)后,此时

r=0,代码继续往下走,走到setHeadAndPropagate(node, r);时,此时node=C,r=0,然后进入setHeadAndPropagate(C , 0),

不会唤醒D,因为r=0,就在此时,线程B调用了release,释放了信号量,此时就会出现明明还有信号量,但是D没有被唤醒。

A,B(获取到信号量),队列中是C->D,此时A释放信号量,唤醒了C,

用于更新头节点,并且唤醒后继共享节点,

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; //老的头节点
    setHead(node);
    if (propagate > 0 ||//信号量还有多余的,那么直接唤醒
        h == null || //不可能发生
        h.waitStatus < 0 ||// SIGNAL 表明必须唤醒后继节点
        (h = head) == null || //不可能发生
        h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
    private void doReleaseShared() {
        for (;;) {
            Node h = head;//保存head临时变量,因为执行下面的代码时可能被其他线程改过了
            
            if (h != null && h != tail) {//链表中还存在等待节点
                int ws = h.waitStatus;
                //理应要唤醒后面的节点,因为SIGNAL的语义就是必须唤醒后面的节点(正常状态)
                if (ws == Node.SIGNAL) {
        //CAS将该状态由SIGNAL改为0,表示唤醒成功,那么如果失败呢?
        //失败的场景就是两个线程同时释放了资源,同时唤醒后面的节点
        //比如A,B两个线程同时调用了Semaphore.release(),队列中head ->C -> D 
        //此时这里只有一个线程可以
        //执行成功,例如A成功了,将旧的头节点的waitStatus更新成了0,那么B就会
        //失败,就会再次进入循环,此时head有两种可能,一种是被更新成了C,
        //因为C被unpark唤醒之后会进到上面代码第12行的for中,此时队列中没变,
        //但是走到 setHead(node);时,将head更新成了C,此时
        //C的ws == Node.SIGNAL,所以会正常走if,【还有一种可能,就是还没执行到
        //setHead(node),此时还是旧的空节点,waitStatus还是0,】
        //就不会再走这个if,而是走else,此时就会将空的head的waitStatus更新为
        //Node.PROPAGATE,此时就会进入if (h == head) ,此时如果head还没更新为C
        //就会一直自旋,直到更新成了C,然后break,此时代码从setHead(node)继续
        //执行,因为Node h = head;此时还是空的头结点,所以会进入h.waitStatus < 0
        //此时空的头结点的状态是Node.PROPAGATE(-3),就会进入if循环,此时Node h = head
        //已经更新成了C,C就会唤醒D;我们回想一下,如果没有下面这个修改成PROPAGATE的步骤,那么
        //回到上面大括号的步骤,此时如果head还没更新为C就break了,此时代码从setHead(node)
        //继续执行,此时因为h.waitStatus=0,所以不会进入if循环,直接就返回了,不会再唤醒D,就会出现
        //明明还有信号量,但是等待的线程却不能被唤醒
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    //唤醒h.next
                    unparkSuccessor(h);
                }
                    //代码走到这,就表明A释放了一个信号量,唤醒C,在C还没执行到setHead(node)时
                    //又有一个信号量释放了,
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue; 
            }
            if (h == head)  
                break;
        }
    }
    public void release() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

Semaphore的acquire就是CAS-1操作

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
    int available = getState();
    int remaining = available - acquires;
    if (remaining < 0 ||
        compareAndSetState(available, remaining))
        return remaining;
}
}

Semaphore的release就是CAS+1操作

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
    int current = getState();
    int next = current + releases;
    if (next < current) // overflow
        throw new Error("Maximum permit count exceeded");
    if (compareAndSetState(current, next))
        return true;
}
}

acquireInterruptibly和acquire的区别就是acquireInterruptibly可以被unpark和中断唤醒,但是acquire只能被unpark唤醒

这里的自旋有个阈值,spinForTimeoutThreshold

private boolean doAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值