深入理解ReentrantLock/ReentrantReadWriteLock

ReentrantLock(可重入锁)

ReentrantLock获取锁和释放锁可以参考这篇博客:https://blog.csdn.net/sophia__yu/article/details/84313234
一:ReentrantLock如何实现可重入

ReentrantLock实现Lock接口的可重入子类。可重入的意思就是一个线程在获取锁的时候,如果该线程已经获取到锁则直接获取成功,不会被阻塞;
由于会获取n次锁,那么在锁释放的时候也需要释放n次,才能完全释放成功。

可重入锁是在tryAcquire()阶段实现的,为什么是tryAcquire阶段可以参考这篇博客:链接
源码如下:

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()) {
            //如果是,再次获取将状态+1,实现可重入
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

如果该同步状态不为0,表示此时同步状态已被线程获取,再判断同步状态的线程是否是当前线程,如果是,同步状态再次加1,并返回true,表示持有线程重入同步块。

可重入锁释放
源码如下:

protected final boolean tryRelease(int releases) {
//释放锁第一步,将同步状态-1
            int c = getState() - releases;
            //如果持有锁线程不是当前线程直接抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果同步状态为0,才将当前持有锁线程置空
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //如果同步状态不为0,更新同步状态,锁没有完全释放
            setState(c);
            return free;
        }

当且仅当同步状态减为0并且持有线程为当前线程时表示正确释放。
否则调用setState()将-1后的状态进行更新。

二:公平锁与非公平锁

synchronized可以实现重入,但是synchronized只能实现非公平锁,而
ReentrantLock即可以实现公平锁也可以实现非公平锁。 那什么是公平锁和非公平锁呢?
如果线程获取锁的顺序与请求等待时间保持一致,满足FIFO,那么这个锁是公平的,否则为非公平锁。
看一系列ReentrantLock源码:

 public ReentrantLock() {
 		//非公平锁
        sync = new NonfairSync();
    }

可以看到ReentrantLock锁默认是非公平锁,但是有一个重载方法实现公平机制:

public ReentrantLock(boolean fair) {
//如果fair是true,是实现公平,否则非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }

要使用公平锁,调用ReentrantLock有参构造传入true,获取内置的公平锁。

非公平锁与公平锁实现区别:
非公平锁:

static final class NonfairSync extends Sync {
        final void lock() {
        //首先先CAS,新启动线程很有可能不入队,直接获取同步状态,那么队头线程依然在等待
            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) {
            //如果同步状态为0,直接CAS,那么新的线程可能会抢占已经排队的线程的锁的使用权,也就是队头线程依然在等待
                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;
        }
}

公平锁:

static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
//再次尝试获取锁
 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //如果同步状态为0,会先判断当前线程是否有前驱结点,有前驱结点即证明有线程在等待,如果有继续等待,否则CAS尝试改变同步状态
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
}

公平锁如何实现公平,非公平锁如何是不公平?

  1. 从获取锁的源码发现,非公平锁的实现在Acquire之前多了一次CAS,如果CAS获取失败才acquire,而公平锁直接acquire;如果直接CAS,会有新启动的线程很有可能直接获取同步状态,队头线程仍然在等待。
  2. 并且在tryAcquire()中,公平锁每一次都会检查队列中该线程结点是否有前驱结点(有前驱结点证明在该线程之前有线程在等待),如果有该线程继续等待,通过这种方式保证先来先服务的原则;而非公平锁,首先直接CAS,这种方式会出现机即使队列中有等待的线程,但是新的线程仍然会与排队线程中的队头线程竞争,所以新的线程可能会抢占已经在排队的线程的锁,这样无法保证先来先服务。
    总结:公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

公平锁与非公平锁比较:

  1. 公平锁保证获取锁的线程一定是等待时间最长的线程,也就是说获取锁的线程一定是同步队列中第一个结点,保证了请求资源时间上的绝对顺序,但是需要频繁的进行上下文切换,性能开销比较大。
  2. 非公平锁有可能新启动的线程先获取在CAS时先获取锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿现象”。但是非公平锁会降低一定的上下文切换,降低性能开销,有更大的吞吐量。

因此,ReentrantLock默认是非公平锁,可以减少上下文切换,降低性能开销,保证了系统更大的吞吐量。

ReentrantReadWriteLock(可重入读写锁)

为什么会有ReentrantReadWriteLock?

我们了解到synchronized和ReentrantLock都是独占式锁。也就是同一时刻只有一个线程获取到锁。但是在一些业务场景中,大部分是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性,如果在读数据很频繁情况下,依然使用独占式锁,会大大降低性能。所以在这种读多写少的情况下,Java提供另一个lock接口的实现子类ReentrantReadWriteLock(可重入读写锁)。
读写锁是允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程与其他的写线程均会阻塞。

那么写线程获取到锁 的前提是:没有任何读写线程获取到锁。
同一时刻可以有多个读线程获取到锁,但并不意味着读锁是无锁,比如当写线程获取到锁,读线程不可以再获取到锁,如果是无锁,读线程可以直接工作,这显然是错误的。

一个例子理解读写锁:
比如说在办黑板报时:小米很开心的办黑板报,但是由于她很害羞,不希望在创作的过程被打扰,也就是她在黑板上写的时候,不能有其他同学在黑板上写,更不能有同学欣赏她的作品,这个过程相当于写锁;而当小米创作结束后,大家都来欣赏漂亮的板报,在大家欣赏的时候,小米没有创作,这个过程相当于读锁。

public interface ReadWriteLock {
    /**
     * 返回读锁
     */
    Lock readLock();

    /**
     * 返回写锁
     */
    Lock writeLock();
}

写锁的获取

写锁的lock方法如下:

public void lock() {
            sync.acquire(1);
        }

AQS的acquire方法如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

从源码可以看见,写锁使用的是AQS的独占模式。首先尝试首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。 (同之前ReentrantLock)
Sync实现了tryAcquire方法用于尝试获取锁,源码如下:

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
             //得到调用lock方法的当前线程
            Thread current = Thread.currentThread();
            int c = getState();
            //得到写锁的个数
            int w = exclusiveCount(c);
            //如果当前有写锁或者读锁
            if (c != 0) {
                // 如果同步状态不为0,但是写锁为0或者当前线程不是独占线程(不符合重入),返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //如果写锁的个数超过了最大值,抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 写锁重入,返回true
                setState(c + acquires);
                return true;
            }
            //如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //否则将当前线程置为获得写锁的线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }

写锁获取源码中需要注意一个方法:exclusiveCount( c ),源码如下:

/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

其 中 EXCLUSIVE_MASK 为 : static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; EXCLUSIVE
_MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与
0x0000FFFF相与,即取同步状态的低16位。因为同步状态的低16位用来表示写锁的获取次数。
而对于读锁来说,同步状态的高16位用来表示读锁的获取次数。

/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }

该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。
在这里插入图片描述

writerShouldBlock( )方法,FairSync的实现是如果等待队列中有等待线程,则返回false,说明公平模式下,只要队列中有线程在等待,那么后来的这个线程也是需要记入队列等待的;NonfairSync中的直接返回的直接是false,说明不需要阻塞。从上面的代码可以得出,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回false,直接通过CAS就可以获得写锁。

获取写锁总结:
当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。

写锁释放

WriteLock的unlock方法如下:

 public void unlock() {
            sync.release(1);
        }

Sync的release方法使用的AQS中的,如下:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

调用tryRelease尝试释放锁,一旦释放成功了,先判断头结点是否有效,最后用unparkSuccessor()启动后续等待的线程。
Sync需要实现tryRelease方法,如下:

 protected final boolean tryRelease(int releases) {
            //如果没有线程持有写锁,但是仍要释放,抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
                // 同步状态减去写状态
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
         //当前写状态是否为0,为0则释放写锁
            if (free)
                setExclusiveOwnerThread(null);
            //更新状态
            setState(nextc);
            return free;
        }

读锁的获取

读锁是一种共享式锁。即同一时刻可以有多个线程获取读锁。
当需要使用读锁时,首先调用lock方法,源码如下:

public void lock() {
            sync.acquireShared(1);
        }

从代码可以看到,读锁使用的是AQS的共享模式,AQS的acquireShared方法如下:

 if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);

当tryAcquireShared()方法小于0时,那么会执行doAcquireShared方法将该线程加入到同步队列中。
Sync实现tryAcquireShared()源码如下:

protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            //如果当前有写线程并且本线程不是写线程,不符合重入,失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //得到读锁的个数
            int r = sharedCount(c);
            //如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值,成功
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果当前读锁为0
                if (r == 0) {
                    //第一个读线程就是当前线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                }
                //如果当前线程重入了,记录firstReaderHoldCount
                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.set(rh);
                    rh.count++;
                }
                return 1;
            }
            //3.否则(读线程应该被阻塞/读锁达到上限/CAS获取失败,循环尝试
            return fullTryAcquireShared(current);
        }

对于readerShouldBlock():对于公平锁,只要队列中有线程在等待,就会返回true,即读线程应该被阻塞;对于非公平锁,如果有线程获取写锁,返回true,否则返回false。如果读线程不被阻塞,那么该读线程将有机会获取到读锁。

读锁获取:
1.当写锁被其他线程获取后,读锁获取失败,也就是如果一个线程获取到写锁后,可以再次获取到读锁(写锁可重入)。
2.如果没有写线程或者该线程就是写线程,将有可能获取到读锁,接下来判断是否应该阻塞阻塞读线程并且读锁的个数是否小于最大值,并且CAS改变同步状态。
3.如果该读线程应该被阻塞或者读锁达到了上线或者CAS失败,有其他线程在并发更新state,那么会调动fullTryAcquireShared方法。

读锁释放

ReadLock的unlock方法如下:

public void unlock() {
            sync.releaseShared(1);
        }

调用了Sync的releaseShared方法,该方法在AQS中提供,如下:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

调用tryReleaseShared方法尝试释放锁,如果释放成功,调用doReleaseShared尝试唤醒下一个节点。
AQS的子类需要实现tryReleaseShared方法,Sync中的实现如下:

 protected final boolean tryReleaseShared(int unused) {
            //得到调用unlock的线程
            Thread current = Thread.currentThread();
            //如果是第一个获得读锁的线程
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            }
            //否则,是HoldCounter中计数-1
            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;
                //如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

读锁的释放:只有当读锁状态为0时,才可以释放成功。
读写锁的应用场景:缓存的实现,代码如下:

package CODE.多线程;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWrite {
    static Map<String,Object> map=new HashMap<>();
    static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
    static Lock readLock=rwl.readLock(); //读锁
    static Lock writeLock=rwl.writeLock(); //写锁

    /**
     * 线程安全的根据一个key获取value
     * @param key
     * @return
     */
    public static final Object get(String key)
    {
        readLock.lock();
        try
        {
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }
    /**
     * 线程安全的根据key设置value,并返回旧的value
     * @param key
     * @param value
     * @return
     */
    public static final Object put(String key,Object value)
    {
        writeLock.lock();
        try
        {
           return  map.put(key,value);
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 线程安全的清空所有value
     */
    public static final void clear()
    {
        writeLock.lock();
        try
        {
            map.clear();
        }finally {
            writeLock.unlock();
        }
    }
}

读写锁降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,
于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:

void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}

总结:
1.公平性选择:支持费公平性(默认)和公平的锁获取方式。
2.重入性:支持重入,读锁获取后可以再次获取读锁,写锁获取之后能够再次获取写锁,同时当前也能获取读锁;
3.锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁。

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/84372445