转载

Java并发编程的艺术(九)——Java中的锁(4)

重入锁ReentrantLock,即支持重进入的锁,它表示同一线程能够重复对资源(锁)进行加锁操作。此外,ReentrantLock还有公平和非公平之分。

1.2 重入锁存在的意义

设想这样一种情况:当独占式锁不支持重入时,在我们已经获得锁的情况下,在同一个线程再次获取该锁,此时获取锁失败并把当前线程加入同步队列中,即线程被自己阻塞。而重入锁就是为解决这个问题的。

1.3 重入锁公平性的理解

先请求锁的线程先获得锁,则锁是公平锁;反之,是不公平锁。事实上,公平锁的效率没有非公平锁高。

1.4 ReentrantLock的非公平锁获取方法nonfairTryAcquire(int acquire)如下

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;
        }
复制代码
  • 1.该方法增加了 再次获取同步状态 的处理逻辑:若当前尝试获取锁的线程为已获得该锁的线程,则获取同步状态成功。
  • 2.成功获取锁的线程再次获取锁成功,则同步状态值增加,这也就要求的释放同步状态时,要减少同步状态值。

1.5 ReentrantLock用于释放锁的方法tryRelease(int releases)如下

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;
        }
复制代码
  • 1.若锁被获取了n次,则前(n-1)调用tryRelease(intreleases)释放锁都将返回false,最后一次调用才返回true。
  • 2.同步状态被完全释放时,锁的占用线程将被设置为null。

1.6 ReentrantLock的tryAcquir(int acquires)方法

该方法用于获取公平锁,与用于获取非公平锁的方法nonfairTryAcquire(int acquire)区别在于公平锁代码里的判断条件 if (compareAndSetState(0, acquires)) 变成了非公平锁代码的 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) ,即增加了方法hasQueuedPredecessors(),该方法用于判断当前节点是否有前驱节点,若返回true,说明有其他线程排在当前节点前边,根据公平锁的定义当前线程还需等待。

2. 读写锁

2.1 读写锁的理解

●读写锁包括 读锁写锁 ,在同一时刻,读写锁允许多个线程获取读锁,但当线程获取写锁时,其他尝试获取读锁或写锁的线程都将阻塞,即写锁是排他锁。

●一般情况下,由于大多数场景读多于写,所以读写锁的性能会比排他锁好。

●Java并发包提供的读写锁实现是ReentrantReadWriteLock。

2.2 ReentrantReadWriteLock的特性

特性 说明
公平性 该锁支持非公平(默认)和公平获取锁。
可重入 该锁是可重入锁。需要注意的是,获取写锁的线程可再次获取写锁,同时也可获取读锁。
锁降级 同一线程按顺序获取写锁、获取读锁、释放写锁,完成这一过程即称之锁降级。

3.读写锁的实现分析

3.1 读写状态的设计

读写锁是依赖同步器(AQS)为基础框架设计的,同步器维护了一个同步状态值(int型)来表示锁的获取与否。而读写锁包含了读锁和写锁,所以同步状态值的高16位表示读状态,低16位表示写状态。

3.2 写锁的获取与释放

●ReentrantReadWriteLock用于获取 写锁 的tryAcquire(int acquires)方法如下:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (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;
        }
复制代码
  • 1.代码 int w = exclusiveCount(c); 表示获取同步状态中表示写写状态的值(即低16位),若该值为0,说明读锁和写锁均没有被获取,因此写锁可直接被获;若该值不为0,说明读锁或者写锁已被获取,执行下一步。
  • 2.代码 if (w == 0 || current != getExclusiveOwnerThread()) 中,w的值为0表示存在读锁,则写锁不能被获取,而对于"||"运算符来讲,当第一个判断条件为false时才会判断第二个条件,也就是说如果程序执行代码 current != getExclusiveOwnerThread() 表示不存在读锁且存在写锁,那么也就需要判断当前线程是否为已获取写锁的线程。
  • 3.存在读锁时,写锁不能被获取的原因在于:若存在读锁且写锁可被获取,则当前线程通过写锁进行了写操作,而正在运行且获取了读锁的其他线程无法感知数据的变化,也就是无法保证共享变量的可见性。

●写锁释放时会减小写状态,当写状态的值为0时表示释放成功。

3.3读锁的获取与释放

●读锁是一个支持重进入的共享锁,它能够被多个线程获取。当 写锁 没有被线程获取时, 读锁 总是能被任意线程获取到;当前线程尝试获取读锁时,如果写锁已被获取过,则当前线程进入等待状态。

●获取读锁的方法tryAcquireShared(int unused)如下( 获取读锁的实现复杂,这里为删减版本 )

protected final int tryAcquireShared(int unused) {
    for (;;) {
        int c = getState();
        int nextc = c + (1 << 16);
        if (nextc < c)
            throw new Error("Maximum lock count exceeded");
        if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
            return -1;
        if (compareAndSetState(c, nextc))
            return 1;
    }
}
复制代码
  • 代码 exclusiveCount(c) 表示获取同步变量的写状态,代码 exclusiveCount(c) != 0 && owner != Thread.currentThread() 表示写锁已被获取且当前线程不是获取写锁的线程,此时获取读锁失败。

3.4 锁降级

●锁降级指的是 当前线程 把持写锁,然后获取读锁,最后释放写锁的过程。

●锁降级的示例如下:

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        // 使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}
复制代码
  • 1.update为volatile变量,当数据发生更改时,该变量被设置为flase,此时所有访问processData()方法的线程都将感应到数据的变化.
  • 2.降级锁中获取写锁后再获取读锁是有必要的,若不这么做,当写锁被释放后再获取读锁,可能有其他线程会获取到写锁并且修改了数据后释放写锁,此时当前线程再来获取读锁时读取到的已经是被修改过的数据,也就是当前线程无法感知其他线程的更新(当前线程需要的是原来的数据,当它不知道数据已经被其他线程更改了)。
  • 3.一般使用降级锁的前提是读优先于写。
原文  https://juejin.im/post/5d00e7f5518825276a2864bd
正文到此结束
Loading...