Java高并发之ReentrantLock、读写锁

1. 可重入锁ReentrantLock
① 重进入
  • 之前在学习同步器时,实现了独占锁Mutex。该锁存在一个缺陷:当一个线程调用Mutex的lock()方法获取锁之后,如果再次调用Mutex的lock()方法获取该锁,该线程将会被阻塞。
  • Mutex不支持重进入,在实现时没有考虑到占有锁的线程再次获取锁的场景
  • synchronized支持隐式的重进入,在递归调用synchronized方法时,不会发生阻塞。
  • 重进入: 任意线程在获取之后仍能再次获取该锁而不会被锁所阻塞。
  • 重进入需要解决的两个问题:
  1. 线程再次获取锁: 锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功。
  2. 锁的最终释放: 线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,用于表示当前线程重复获取锁的次数;释放锁时,计数自减计数值为0表示锁已经成功释放
  • 重进入的计数方法,是通过同步状态实现的。
  • 可重入锁ReentrantLock的两个特性:
  1. 支持重进入
  2. 支持线程获取锁时的公平性与非公平性选择
② ReentrantLock的非公平性访问
  • ReentrantLock通过带fair参数的构造函数,创建支持公平或者非公平获取策略的锁实例。默认为false,即非公平性。

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
  1. 根据fair参数的值,为ReentrantLock创建公平或者非公平的的同步器。
  2. 公平的同步器,支持同步队列中的线程按照FIFO原则获取锁
  3. 非公平的同步器,同步队列中的线程在锁可用时,都可以争抢获取锁的资格。可能使得先阻塞的线程最后才能获取到锁。
  • 非公平获取锁,是由同步器的tryAcquire()方法调用nonfairTryAcquire()方法实现的。

    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. 先通过getState()方法获取同步状态值。
  2. 如果该值为0,说明没有线程占用该锁。直接通过CAS方法(compareAndSetState())设置锁的同步状态,并设置当前线程为锁的持有者。返回true,表示成功获取锁。
  3. 如果同步状态值不为0,判断当前线程是否为持有锁的线程(current == getExclusiveOwnerThread())。如果是,说明占有锁的线程再次获取锁,直接增加同步状态值并返回true,表示获取锁成功,
  4. 不满足上面的两种情况,返回false,表示获取锁失败。
  • 占有锁的线程再次获取锁,只是将同步状态值增加。因此,该线程在释放锁时,需要将同步状态值减少。

  • 锁的释放,不管是公平性还是非公平性,都通过tryRelease()方法实现锁的释放。

    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次nonfairTryAcquire()方法。释放时,需要调用n次tryRelease()方法。
  2. 前n-1次,同步状态值均不为0,更新同步状态的值并返回false,表明锁未完全释放
  3. 第n次,同步状态值变为0设置占有锁的线程为null,并返回true表明锁已经完全释放
  • 关于nonfairTryAcquire(acquires)方法和tryRelease(releases)方法的参数:
  1. 调用时,acquires = 1releases = 1。表明线程重复获取锁时,同步状态值加1;线程释放锁时,同步状态的值减1。
  2. 当同步状态的值为0,表示锁被完全释放。
③ ReentrantLock的公平访问
  • 公平访问,要求获取锁的线程必须是同步队列中头结点所包含的线程,这样能保证锁的获取按照FIFO规则

  • ReentrantLock的公平访问,调用公平同步器的tryAcquire()方法实现。

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            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. 与非公平获取锁的nonfairTryAcquire()方法相比,公平获取锁在CAS设置同步状态值时,要求线程所在的同步节点没有前驱节点
  2. 即公平获取锁,条件判断时多了!hasQueuedPredecessors()方法。如果该方法返回true,说明有线程比当前线程更早的发起了获取锁的请求,为了保证FIFO规则,当前线程无法获取锁。
④ 公平性锁和非公平性锁
  • 可重入锁ReentrantLock,可以根据构造函数参数fair的值,构造公平性的可重入锁或者非公平性的可重入锁。
  • 两种锁的特点:
  1. 公平性锁,每次都是同步队列中的第一个节点的线程获取到锁;非公平性锁,一个线程可以连续获取锁。
  2. 公平性锁保证了锁的获取按照FIFO规则,而代价是进行大量的线程切换。非公平性锁可能造成线程饥饿,但极少进行线程切换,具有更大的吞吐量
⑤ synchronized和ReentrantLock的比较与选择
  • synchronizedReentrantLock的区别:
  1. 锁的实现: synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  2. 锁的性能: 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronizedReentrantLock 性能大致相同
  3. 是否可中断: ReentrantLock可以响应中断,而synchronized 不行。
  4. 公平锁与非公平锁: synchronized 中的锁是非公平的,ReentrantLock 支持公平锁和非公平锁两种,默认情况下是非公平的。
  5. 是否可以绑定多个condition对象: 一个 ReentrantLock 可以同时绑定多个 Condition 对象。比如ArrayBlockingQueue中的 ReentrantLock 就绑定了两种Condition对象,一个是notFull,一个是notEmpty。
  • 使用时的选择:
  1. 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized
  2. 因为 synchronized 是 JVM 提供的一种锁机制,JVM 原生地支持它;而 ReentrantLock 并非所有的 JDK 版本都支持。
  3. synchronized锁的获取和释放由JVM隐式完成,不用担心没有释放锁而导致死锁问题
2. 读写锁
① 读写锁概述
  • ReentrantLock可重入锁,也是公平/非公平锁,同时还是排他锁
  • 之前提到的MutexReentrantLock都是排它锁,即同一时刻只能有一个线程可以获取到锁。
  • 读写锁是一种特殊的锁,它包含一个读锁,一个写锁。同一时刻允许多个读线程访问;写线程访问时,所有的读线程和其他的写线程均被阻塞。
  • 总结:
  1. 读写锁是一种特殊的锁,它包含一个读锁,一个写锁。读锁是支持重进入的共享锁写锁是支持重进入的排它锁
  2. 由于大多数场景都是多读少写,读写锁比排它锁具有更好并发性和吞吐量
  3. JUC包中,ReentrantReadWriteLock是读写锁的实现。
  • ReentrantReadWriteLock的特性:
  1. 公平性选择: ReentrantReadWriteLock支持公平和非公平获取锁的方式,默认情况为false,即非公平。非公平比公平的吞吐量更高。
  2. 重进入:ReentrantReadWriteLock支持重进入,读线程获取读锁后,能再次获取读锁写线程获取写锁后,能再次获取写锁或读锁
  3. 锁降级: 获取写锁、获取读锁再释放写锁,这时写锁能够降级为读锁
② ReentrantReadWriteLock的接口与示例
  • ReentrantReadWriteLock实现了ReadWriteLock接口ReadWriteLock接口只有两种方法: readLock()方法和writeLock()方法,分别用于返回读锁和写锁。

  • ReentrantReadWriteLock的构造方法如下:

    public ReentrantReadWriteLock() {}
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    
  1. 无参构造函数,默认创建非公平的读写锁。
  2. 参数fair用于指明构造公平还是非公平的读写锁,公平的读写锁对应公平同步器FairSync,非公平的读写锁对应非公平的同步器NonfairSync
  3. 读锁是ReadLock的实例,写锁是WriteLock的实例,通过readLock()writeLock()可以分别获取读锁或写锁。
  • ReentrantReadWriteLock用于展示内部工作状态的方法:
  1. int getReadLockCount(): 返回读锁被获取的次数。读锁被获取n次由于支持重进入,持有读锁的线程数小于等于 n
  2. int getReadHoldCount(): 返回当前线程获取读锁的次数。
  3. boolean isWriteLocked(): 判断写锁是否被获取。
  4. int getWriteHoldCount(): 返回当前线程获取写锁的次数。如果当前线程占有写锁,返回实际获取次数;如果当前线程未占有写锁,返回0。
  • 使用读写锁+HashMap实现缓存:

    public class Cache {
        private HashMap<String, Integer> map;
        private ReentrantReadWriteLock lock;
        private Lock readLock;
        private Lock writeLock;
        public Cache() {
            map = new HashMap<>();
            lock = new ReentrantReadWriteLock();
            // 分别获取读锁、写锁
            readLock = lock.readLock();
            writeLock = lock.writeLock();
        }
        public Integer get(String key) {
            readLock.lock();
            try {
                return map.get(key);
            } finally {
                readLock.unlock();
            }
        }
        public Integer put(String key, Integer value) {
            writeLock.lock();
            try {
                return map.put(key, value);
            } finally {
                writeLock.unlock();
            }
        }
        public void clear() {
            writeLock.lock();
            try {
                map.clear();
            } finally {
                writeLock.unlock();
            }
        }
    }
    
  1. get()方法获取元素时,通过读锁保证多线程并发读缓存
  2. put()方法和clear()方法,通过写锁保证同一时刻只有一个线程能写缓存
③ 读写锁的读写状态设计
  • 读写锁同样依赖自定义的同步器(公平或非公平)实现同步功能,读写步状态就是同步器的同步状态。
  • 同步器的同步状态只有一个,而读写锁包含读锁和写锁。因此,需要将32 bit的同步状态进行分割(按位切割使用),高16 bit用于表示读状态低16 bit表示写状态
  1. 上图中,读状态和写状态都不为0表明有线程获取了写锁,而且重进入了两次;该线程还获取了两次读锁。
  2. 获取写状态:S & 0x0000FFFF,高16 bit清零;获取读状态 S >>> 16,高16 bit无符号右移到低16 bit。
  3. 增加写状态:S + 1,增加读状态:S + (1 << 16),即S + 0x00010000
  • 关于状态的判断:如果S不为0,写状态为0,则读状态大于0读锁已被获取
④ 写锁的获取与释放
  • 写锁是支持重进入排它锁
  • 写锁获取的条件:
  1. 同步状态不为0: 若读锁已经被获取,或当前线程不是已经获取到读锁的线程,则当前线程不能获取写锁并进入阻塞状态;若当前线程为已经获取写锁的线程,直接调用setState()增加写状态。
  2. 同步状态为0: 通过compareAndSetState()方法设置写状态,并设置当前线程为持有写锁的线程。
  • 调用tryAcquire()方法,可以实现写锁的获取:

    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();
        int w = exclusiveCount(c);
        if (c != 0) {
            // 写状态为0,同步状态不为0,则读状态不为0,读锁已经被获取
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // 持有写锁并再次获取写锁
            setState(c + acquires);
            return true;
        }
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
    
  • ReentrantLock中非公平获取锁的方法相比,当同步状态不为0时,除了判断当前线程是否为持有写锁的线程,还需要增加一个判断条件:读锁是否已经被获取

  • 读锁被获取后,写锁不能被获取的原因:读锁需要保证写锁的操作对读锁可见。即有读线程已经获取到读锁,如果写线程再获取到写锁,则读线程不会阻塞。因此,读线程无法感知到写线程的操作,不能保证读取到的数据是最新的。

  • 写锁的释放:

  1. 获取了写锁n次,则需要释放写锁n次。
  2. 当写状态为0时,写锁被释放,需要设置持有写锁的线程为null
⑤ 读锁的获取和释放
  • 读锁是支持重进入共享锁

  • 调用tryAcquireShared()方法,可以获取读锁:

    // 只展示tryAcquireShared()方法的关键代码
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c); // 获取同步状态中的读状态
    
  1. 如果其他线程获取到了写锁,则当前线程无法获取到读锁并阻塞
  2. 如果当前线程获取到了写锁,或者写锁未被任何线程获取,则当前线程可以获取到读锁
  3. 当前线程获取到读锁,通过compareAndSetState()实现读状态的增加,每次增加1 << 16
  4. 由于同一时刻可能有多个线程获取到读锁,依靠CAS保证线程安全
  • 读锁的释放: 读状态用同步状态的高16 bit表示,释放读锁时,减少的同步状态值不是1,而是1 << 16
⑥ 锁降级与锁升级
  • 锁降级: 获取写锁,获取读锁,释放之前获取的写锁。这时,写锁降级为读锁
  • 锁升级: 获取读锁,获取写锁,释放之前获取的读锁。这时,读锁升级为写锁
  • 锁降级与锁升级都是为了保证数据的可见性
  • ReentrantReadWriteLock只支持锁降级,不支持锁升级。原因:
  1. 读写锁中的读锁是共享锁,多个线程获取到读锁,其中某个线程获取写锁,则其他线程不会发生阻塞。
  2. 这时,该线程写锁的操作对其他线程是不可见的,即其他线程无法获取到该线程对数据的更新
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值