zoukankan      html  css  js  c++  java
  • 并发编程之——写锁源码分析

    1.前言

    Java 中的读写锁实现是 ReentrantReadWriteLock ,是一种锁分离策略。能有效提高读比写多的场景下的程序性能。

    关于如何使用参见 并发编程之 Java 三把锁

    由于读写锁较为复杂,故分为篇文章进行源码分析,今天先说较为简单的写锁。

    2. 写锁介绍

    不论是读锁还是写锁,都是基于 AQS 的,而 AQS 留给子类实现的就是 tryAcquire 或者 tryAcquireShared 方法,前者是写锁的实现,后者是读锁的实现,从名字上可以看出,一个是独占锁,一个是共享锁。

    今天看的就是 tryAcquire 和 tryRelease 的实现,获取和释放。

    3. tryAcquire 实现

    源码加注释如下:

    // 如果有读锁,此时是获取不到写锁的。当有写锁时,判断重入次数。
    // 当写锁空闲,读锁空闲,公平模式下,如果队列中有等待的,不会抢锁。非公平模式下,必抢锁。
    protected final boolean tryAcquire(int acquires) {
        // 写
        Thread current = Thread.currentThread();
        int c = getState();
        // 用 state & 65535 得到低 16 位的值。
        int w = exclusiveCount(c);
        if (c != 0) {
            // (Note: if c != 0 and w == 0 then shared count != 0)
            // 如果 state 不是0,且低16位是0,说明了什么?说明写锁是空闲的,读锁被霸占了。那么也不能拿锁,返回 fasle。
            // 如果低 16 位不是0,说明写锁被霸占了,并且,如果持有锁的不是当前线程,那么这次拿锁是失败的。返回 fasle。
            // 总之,当有读锁,就不能获取写锁。当有写锁,就必须是重入锁。
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            // 到这一步了,只会是写重入锁。如果写重入次数超过最大值 65535,就会溢出。
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            // 将 state  + 1
            setState(c + acquires);
            return true;
        }
        // 当 state 是 0 的时候,那么就可以获取锁了。
        // writerShouldBlock 判断是否需要锁。非公平情况下,返回 false。公平情况下,根据 hasQueuedPredecessors 结果判断。
        // 当队列中有锁等待了,就返回 false 了。
        // 当是非公平锁的时候,或者队列中没有等待节点的时候,尝试用 CAS 修改 state。
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        // 修改成功 state 后,修改锁的持有线程。
        setExclusiveOwnerThread(current);
    

    详解的逻辑都写在注释里了,可以对照源码查看,这里再次总结这个方法的逻辑。

    1. 首先判断锁是否空闲。

    2. 如果空闲,则根据公平与否判断是否应该获取锁,当 writerShouldBlock 返回结果是 false 的时候,就使用 CAS 修改 state 变量,获取锁。成功之后修改 AQS 持有线程。

    3. 如果不是空闲的,则判断写锁是否是空闲的,这里有 2 种情况:

      3.1 如果写锁空闲,但 state 不是0,说明有读锁,那么就不能获取锁。

      3.2 如果写锁不空闲,判断持有 AQS 锁的线程是不是当前线程,如果不是,不能获取,反之,可以获取重入锁。

    4. tryRelease 实现

    代码加注释如下:

    protected final boolean tryRelease(int releases) {
        // 是否持有当前锁
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 计算 state 值
        int nextc = getState() - releases;
        // 计算写锁的状态,如果是0,说明是否成功。
        boolean free = exclusiveCount(nextc) == 0;
        // 释放成功,设置持有锁的线程为 null。
        if (free)
            setExclusiveOwnerThread(null);
        // 设置 state
        setState(nextc);
        return free;
    }
    

    这里还是很简单的,只是有一个地方需要注意:

        // 计算写锁的状态,如果是0,说明是否成功。
        boolean free = exclusiveCount(nextc) == 0;
    

    这里计算的只是 state 变量的低 16 的值,而不是整个 state 的值。虽然写的时候,必然是串行的,但这里计算的仍然是低 16 位的。

    5. 总结

    写锁在获取锁的时候,有几个地方需要注意:当有读锁的时候,是不能获取写锁的。写锁可以重入,但不能超过 65535 次。写锁状态设置在 state 变量的低 16 位。

    同时,在获取锁的时候,也会根据公平与否决定此次释放需要获取锁。

    如果是非公平的,直接尝试CAS 修改 state ,获取锁。

    如果是公平的,则根据 hasQueuedPredecessors 方法的返回值判断,也就是如果队列中有等待的线程,则依据公平策略,放弃此次获取锁的操作。反之,直接获取锁。

  • 相关阅读:
    【Vijos-P1285】佳佳的魔法药水-Dijkstra思想
    【NOIP2009提高组T3】最优贸易-双向SPFA
    【NOIP2009提高组T3】最优贸易-双向SPFA
    【Vijos-P1046】观光旅游-Floyd求最小环
    【Vijos-P1046】观光旅游-Floyd求最小环
    【Vijos-P1060】盒子-DP+组合数学
    mysql 结合keepalived测试
    set global read_only=0; 关闭只读,可以读写 set global read_only=1; 开始只读模式
    set global read_only=0; 关闭只读,可以读写 set global read_only=1; 开始只读模式
    -F, --flush-logs
  • 原文地址:https://www.cnblogs.com/stateis0/p/9062057.html
Copyright © 2011-2022 走看看