重入锁 ReentrantLock 和 读写锁 ReentrantReadWriteLock

一、重入锁 ReentrantLock

ReentrantLock 是AQS的经典应用,对AQS不了解的可以看 此博客

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer

ReentrantLock 实现了Lock接口,并且内部组合了同步组件并且重写了其方法

1. 实现重进入

重进入是指任意线程在获取到锁之后能够再次获取锁而不会被锁阻塞,实现该特性主要需要考虑两个问题

  1. 线程再次获取锁:锁需要识别获取锁的线程是否为当前占用锁的线程,是的话就允许重入
  2. 锁的最终释放:线程如果总共获取了N次锁,那么就应该释放N次锁之后才能被其他线程获取该锁,需要对锁的获取和释放进行计数。

以 ReentrantLock 的**非公平锁(默认状态)**为例,看看它是如何实现锁的获取和释放

① ReentrantLock 非公平获取锁 nonfairTryAcquire(int acquires)

        final boolean nonfairTryAcquire(int acquires) {
        	// 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取状态
            int c = getState();
            // 如果状态是0 表明锁没有被获取
            if (c == 0) {
            	// 通过CAS 改变状态,成功则当前线程获取锁
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果状态不是 0 ,并且当前线程就是锁持有线程,说明是锁重入
            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;
        }

② ReentrantLock 非公平释放锁 tryRelease(int releases)

        protected final boolean tryRelease(int releases) {
        	// 获取 当前状态减去释放锁数的值
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果c 为0 说明锁完全释放,将持有锁线程置null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 所有操作完成后 设置状态为0,可以让其他线程抢占锁
            setState(c);
            return free;
        }

2. 公平和非公平获取锁的区别

上文的非公平获取锁只要CAS成功设置同步状态,则表示获得锁

而公平锁则不是,公平锁应该严格遵守时间顺序,先进先出

具体实现如下

和非公平获取锁的区别仅在于 !hasQueuedPredecessors()

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 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;
        }


    public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

二、读写锁 ReentrantReadWriteLock

之前提到的锁,基本都是排它锁,同一时刻只能允许一个线程访问,而读写锁 ReentrantReadWriteLock 在同一时刻,允许多个线程访问,但是在写线程访问时,所有其他线程均被阻塞。

读写锁维护了一个读锁一个写锁,通过分离读锁和写锁模式的并发性比一般的排它锁更高(大多数场景,读是多于写的)

ReentrantReadWriteLock 的特性

  • 公平性的选择:支持非公平(默认)和公平的锁获取方式
    • 非公平锁的吞吐量优于公平锁,因为比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗
  • 重进入:该锁支持重进入,以读写线程为例子,读线程在获取了读锁之后,能够再次获取读锁。而写线程获取了写锁之后,能再次获取写锁,也能再次获取读锁
  • 锁降级:写锁可以降级为读锁,但是读锁不能升级为写锁,也就是说,可以获取写锁,获取读锁,释放写锁。但是不能获取读锁未释放读锁之前,获取写锁

1. 读写锁的应用案例

通过一个模拟数据库缓存的案例说明读写锁的使用方式

/**
 * 模拟缓存池
 */
class CachePool {

    /**
     * 具体存放缓存数据
     */
    static Map<String, Object> map = new HashMap<>();

    /**
     * 读写锁
     */
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    private MapperDemo mapperDemo;

    public Object get(String sql) {
        r.lock();
        try {
            // 先查缓存,如果缓存有直接返回
            Object value = map.get(sql);
            if (value != null) {
                return value;
            }
        }finally {
            r.unlock();
        }
        w.lock();
        try {
            Object value = map.get(sql);
            if (value == null) {
                // 再次检查,如果缓存没有则查询数据库
                 value = mapperDemo.getOne(sql);
                 map.put(sql,value);
            }
            return value;
        }finally {
            w.unlock();
        }

    }

    public int update(String sql, Object value) {
        w.lock();
        try {
            // 先更新数据库
            int update = mapperDemo.update(sql, value);
            // 再清除缓存,避免先清除缓存后其他线程在缓存中加入脏数据,数据库再更新
            if (update == 1) {
                map.clear();
            }
            return update;
        }finally {
            w.unlock();
        }
    }

}

/**
 * 模拟持久层
 */
class MapperDemo {

    Object getOne(String sql) {
        return new Object();
    }

    int update(String sql,Object value) {
        return 1;
    }
}

上述例子中,读get(String sql)操作需要获取读锁,写操作需要获取写锁,获取写锁时其他线程想获取读锁或者写锁都会被阻塞。

2. 读写锁的实现原理

2.1 读写状态的设计

读写锁同样依赖自定义同步器来实现同步功能,读写状态就是同步器的同步状态 state 该整型变量维护这多个读线程和一个写线程

如何在一个整型变量上维护多种状态呢,当然就是使用按位切分,读写锁将变量切分成了两个部分,高16位表示读,低16 位表示写

那么读写锁是如何确定读写状态的呢?

		static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • 写状态 等于
    • EXCLUSIVE_MASK 等于1 左移 16 位 等于 10000000000000000
    • 再 减 1 等于 1111111111111111
    • 状态 c & EXCLUSIVE_MASK 相当于将高十六位全部抹去,通过低十六位计算写状态
  • 读状态等于
    • c >>> SHARED_SHIFT 状态c 无符号补0 右移十六位,相当于取高十六位

2.2 写锁的获取与释放

① 写锁的获取 tryAcquire(int acquires)

        protected final boolean tryAcquire(int acquires) {
			// 获取当前线程
            Thread current = Thread.currentThread();
            // 获取状态
            int c = getState();
            // 获取写状态
            int w = exclusiveCount(c);
            
            if (c != 0) {
                // 如果状态c不为0,但是写状态w为0, 说明读锁已经获取
                // 或者当前线程不是拥有锁线程 则返回false 获取锁失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 写线程重入次数超出MAX_COUNT 抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 否则就说明是锁重入,则更新状态并且获取锁成功
                setState(c + acquires);
                return true;
            }
            // 如果状态c等于0,说明写锁读锁都没有被获取
            // 如果写锁需要被阻塞 或者通过CAS判断 期间有其他线程获取锁,则获取写锁失败
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            // 以上都没有问题,则设置获取写锁线程
            setExclusiveOwnerThread(current);
            return true;
        }

② 写锁的释放

        protected final boolean tryRelease(int releases) {
        	// 如果当前线程不是拥有写锁的线程,则抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 获取原先的状态减去 释放锁的状态得到当前状态
            int nextc = getState() - releases;
            // 获取写状态是否等于0
            boolean free = exclusiveCount(nextc) == 0;
            // 如果等于0,则写锁全部释放,将拥有写锁线程置null
            if (free)
                setExclusiveOwnerThread(null);
            // 更新状态
            setState(nextc);
            return free;
        }

2.3 读锁的获取与释放

① 读锁的获取 tryAcquireShared(int unused)

        protected final int tryAcquireShared(int unused) {
        	// 获取当前线程
            Thread current = Thread.currentThread();
            // 获取状态
            int c = getState();
            // 如果已有线程获取写锁,并且不是当前线程则返回 -1 尝试获取读锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 获取读状态
            int r = sharedCount(c);
            // 如果读不需要被阻塞,并且读锁个数小于MAX_COUNT 并且没有线程在期间对状态进行改变导致CAS失败
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果读状态是0,说明没有线程获得读锁,
                // 将当前线程置为第一个读并且将第一个读的持有数量 + 1
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                // 如果第一个读线程就是当前线程,则持有数量+ 1
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                // 其他情况则将当前线程增加一次获取读锁次数
                // 每个线程获取读锁的次数由ThreadLocal保存,由线程维护
                // 读状态是所有线程读次数的综合
                } 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;
            }
            return fullTryAcquireShared(current);
        }

① 读锁的释放tryReleaseShared(int unused)

        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;
            }
        }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值