【多线程】读写锁ReentrantReadWriteLock源码分析

news2025/8/2 20:40:15

【多线程】读写锁ReentrantReadWriteLock源码分析

  • (一)读写锁ReentrantReadWriteLock源码分析
    • 【1】类图结构和参数
    • 【2】写锁的获取和释放
      • (1)lock方法
      • (2)lockInterruptibly方法
      • (3)tryLock方法
      • (4)unlock方法
    • 【3】读锁的获取和释放
      • (1)lock方法
      • (2)lockInterruptibly方法
      • (3)tryLock方法
      • (4)unlock方法
  • (二)读写锁ReentrantReadWriteLock使用案例
    • (1)案例一:对缓存token的读写
    • (2)案例二:测试

ReentrantLock完全可以解决线程安全问题,但是ReentrantLock是独占锁,某一时间只能有一个线程可以获取锁,其他线程只能等待。现实中有很多“写少读多”的场景,那么我们就希望同一时间只能有一个写线程可以获取写锁,但是同时可以多个读线程可以获取读锁,于是便有了ReentrantReadWriteLock。ReentrantReadWriteLock采用读写分离策略,允许多个线程可以同时获取读锁。

(一)读写锁ReentrantReadWriteLock源码分析

【1】类图结构和参数

读写锁的内部维护了一个读锁ReadLock和一个写锁WriteLock,它们依赖Sync实现具体的功能。而Sync继承自AQS,并且也提供了公平锁和非公平锁的实现。AQS中只维护了一个state状态,而ReentrantReadWriteLock需要维护读状态和写状态,一个state怎么表示写和读两种状态呢?其实就是使用state的高1位表示读状态,获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数。
在这里插入图片描述

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

【2】写锁的获取和释放

(1)lock方法

写锁是个独占锁,某时只有一个线程可以获取锁,如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。

另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数+1后直接返回。

在这里插入图片描述
在这里插入图片描述
【1】tryAcquire方法

/**
 * 这个方法是写锁调用才会执行的
 * 不是第一次加写锁
 * 因为是写锁才调用的方法,因此只需要排除前面加的都是读锁这种情况即可,也就是 c!=0 但 w==0的情况
 * 先判断是否加了锁c!=0,如果加了锁也就是c!=0内部的这个分支,
 * 是否没有加过写锁
 * 是否是重入写锁
 * 是第一次加写锁
 */
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();// 获取当前线程对象
    int c = getState(); // 获取state(前16位是读锁个数、后16位是写锁个数)
    int w = exclusiveCount(c);// 获得写锁的个数,w有write的含义。这个值就是写锁的个数,通过按位与 15 得到写锁个数
    if (c != 0) {// c!=0则说明加过锁
        // 如果写锁个数为0 (说明加的都是读锁,不需要阻塞因此抢占锁失败) 或者 当前线程不是持有写锁线程(w!=0说明加过写锁需要判断当前线程是否是持有写锁的那个线程,不是则说明抢占锁失败)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false; // 表示抢占锁失败,这里导致了两种情况,一种是加的都是读锁,一种是加了写锁,但当前线程不是持有锁线程
        // 执行下面的判断都表示加过了写锁,相当于写锁的重入,因此需要将写锁计数相加也就是判断里的操作
        if (w + exclusiveCount(acquires) > MAX_COUNT) // 说明是重入锁,判断本次加了acquires次锁后锁计数是否超过最大值 2的16次方-1
            throw new Error("Maximum lock count exceeded");// 超过能加写锁的最大值则抛异常
        // 写锁重入,因此保留读锁加上写锁重入的acquires次,将state更新
        setState(c + acquires);
        return true;//返回true说明加锁成功
    }
    // 前面没有加过锁,需要加写锁,尝试利用CAS操作更新state进行加锁,实际上逻辑上不需要这里的if,但是应该是由于并发问题怕中途state值被改了,因此CAS操作可能失败(所以失败则return false)
    // c==0 说明没有加过锁,尝试将state从0更新为acquires,更新成功则说明加锁成功,因此不会返回false,而是执行后面的return true
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);// 将当前线程设置为独占线程,表示加写锁成功!
    return true;// 加锁成功
}

【2】exclusiveCount方法
直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示。

static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/**
 * 返回写锁的个数
 */
static int exclusiveCount(int c) {
    return c & EXCLUSIVE_MASK;
}

【3】writerShouldBlock方法
FairSync公平锁的写法

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

NonfairSync非公平锁写法

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        /* As a heuristic to avoid indefinite writer starvation,
         * block if the thread that momentarily appears to be head
         * of queue, if one exists, is a waiting writer.  This is
         * only a probabilistic effect since a new reader will not
         * block if there is a waiting writer behind other enabled
         * readers that have not yet drained from the queue.
         */
        return apparentlyFirstQueuedIsExclusive();
    }
}

【4】获取写锁的步骤
(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。

(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。

(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。

(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
在这里插入图片描述

(2)lockInterruptibly方法

(3)tryLock方法

在这里插入图片描述

/**
 * 执行tryLock进行写入,从而在两种模式下都可以进行插入。 这与tryAcquire的作用相同,只是缺少对writerShouldBlock的调用。
 */
@ReservedStackAccess
final boolean tryWriteLock() {
    Thread current = Thread.currentThread(); // 得到当前线程
    int c = getState(); // 得到锁计数
    if (c != 0) { // 不为0说明加过锁
        int w = exclusiveCount(c); // 得到写锁次数
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;// 写锁被其它线程占用,当前线程抢占写锁失败
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    // 第一次就加写锁,cas更新state值
    if (!compareAndSetState(c, c + 1))
        return false;
    // 将当前线程设置为独占
    setExclusiveOwnerThread(current);
    return true;// 写锁加锁成功!
}

(4)unlock方法

在这里插入图片描述

/**
 * 尝试释放锁
 */
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())//判断当前现是否是持有锁线程如果是则不执行,如果不是则需要抛异常,因为当前线程没有持有锁
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;// 计算释放锁释放合法,不允许释放超过加锁次数
    boolean free = exclusiveCount(nextc) == 0;//
    if (free) // 判断释放锁后释放锁计数是否为0,为0则说明当前线程不再是持有锁线程将其从排他线程状态清除
        setExclusiveOwnerThread(null);
    setState(nextc);// 更新锁次数计数
    return free;//如果释放锁后计数为0则返回true,否则返回false
}

写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下。
在这里插入图片描述

【3】读锁的获取和释放

(1)lock方法

在这里插入图片描述

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

【1】tryAcquireShared方法

/**
 * 读锁才调用的方法,当前线程尝试获取读锁
 */
@ReservedStackAccess
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread(); // 获取当前线程
    int c = getState();// 获取存有读和写锁次数的state值
    /**
     * 是写锁则进入
     */
    // 通过exclusiveCount(c)得到写锁次数,如果不为0则说明加了写锁。加了写锁需要判断当前线程是否是持有写锁的线程,是则不返回-1,不是则说明是写读状态需要进行阻塞当前线程
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1; // 说明是写读状态、返回-1,抢占读锁失败
    // 执行到这里说明前面没有加过写锁,可能加过读锁
    int r = sharedCount(c); // 获取加的读锁次数,r就是read,实际就是将state右移16位得到
    // 到这里说明没有加过锁,到这里c是0,因此进行加锁操作将state更新为读锁的1 实际二进制是:0000 0000 0000 0001 0000 0000 0000 0000
    /**
     *  是读锁,
     *  一、读是共享的情况直接执行if内
     */
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) { // 第一次进入,因为能到达这里就说明没有写锁,有判断r==0则说明读锁也为0,则说明是第一次调用
            firstReader = current; // 将第一个线程存起来
            firstReaderHoldCount = 1;// 计数为1
        } else if (firstReader == current) {
            firstReaderHoldCount++; // 读重入,读锁计数进行累加
        } else {
            // 说明不是获得读锁的线程进来了
            // tid 为key ,value为读锁次数
            HoldCounter rh = cachedHoldCounter;// 将当前线程初始值是null
            // 第一次null直接创建一个
            if (rh == null || rh.tid != LockSupport.getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();// 通过ThreadLocal得到HoldCounter(计数保持器,内部存了加锁计数)
            else if (rh.count == 0) // 如果锁计数为0
                readHolds.set(rh); // 更新锁计数保持器对象
            rh.count++; // 计数累加
        }
        return 1;// 表示抢占读锁成功
    }
    /**
     * 二、读是排他的情况,调用下面这个方法
     */
    return fullTryAcquireShared(current);
}

【2】doAcquireShared方法

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

【3】sharedCount方法
表示占有读锁的线程数量,直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的第十六位表示写锁数量。

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

【4】fullTryAcquireShared方法

/**
 * 读是排他的情况采用自旋方式
 * 完整版本的获取读,可处理CAS错误和tryAcquireShared中未处理的可重入读。
 */
final int fullTryAcquireShared(Thread current) {
    /**
     * 该代码与tryAcquireShared中的代码部分冗余,但由于不使tryAcquireShared与重试和延迟读取保持计数之间的交互复杂化,因此整体代码更简单。
     */
    HoldCounter rh = null;
    for (; ; ) {// 自旋
        int c = getState(); // 获取读写锁计数
        /**
         * 如果存在写锁
         */
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)// 判断当前线程是否是持有同一把写锁的线程
                return -1;// 加锁失败,当前线程不是持有写锁线程
        }
        /**
         * 不存在写的情况
         */
        // 1.判断读是否是排他的,如果是则进入
        else if (readerShouldBlock()) {
            // 当前线程是不是第一个读锁线程,是则说明当前线程是重入的读锁线程
            if (firstReader == current) {
                // 什么也没有
            } else {
                // 如果当前线程不是第一个抢占到读锁的线程,如果锁计数存在
                if (rh == null) {
                    rh = cachedHoldCounter;  // 得到锁计数保持器
                    if (rh == null || rh.tid != LockSupport.getThreadId(current)) {
                        rh = readHolds.get(); // 得到锁计数保持器
                        if (rh.count == 0) // 如果计数为0
                            readHolds.remove(); // 清除保持器
                    }
                }
                // 读锁计数保持器存在,如果等于0则抢占读锁失败,因为这个计数器在tryAcquireShared方法已经被赋值了,所以不会为0,为0说明cas操作失败了
                if (rh.count == 0)
                    return -1; // 加锁失败,当前线程
            }
        }
        // 2.到这里说明是共享的读
        /**
         * 注意:
         *  如果是tryAcquireShared方法过来的其实下面不会执行到的,
         *  因为在tryAcquireShared方法中已经走过一遍这个逻辑了,
         *  这里加上这个逻辑只是处于对当前方法的封装,这样当前方法可以不用依赖tryAcquireShared方法
         */
        if (sharedCount(c) == MAX_COUNT) // 判断读锁是否超过最大值
            throw new Error("Maximum lock count exceeded");
        // 读共享,因此只需要通过cas将读锁计数累加1即可,因为CAS操作多以是单线程所以是加1
        if (compareAndSetState(c, c + SHARED_UNIT)) {// 更新state值
            // c 一开始是0,因为上面更新的不是c而是state值,如果c是0说明是第一个线程调用了这个方法,执行到了这里
            if (sharedCount(c) == 0) {
                firstReader = current; // 保存当前的第一个线程
                firstReaderHoldCount = 1;// 保存计数(因为是第一次进入所以是1)
            } else if (firstReader == current) {
                firstReaderHoldCount++; // 持锁的同一个线程重入读锁
            } else {
                if (rh == null)
                    rh = cachedHoldCounter; // 其它线程尝试获取读锁,获取第一个线程产生的HoldCounter对象
                if (rh == null || rh.tid != LockSupport.getThreadId(current))
                    rh = readHolds.get(); // 从ThreadLocal中获取HoldCounter对象
                else if (rh.count == 0)
                    readHolds.set(rh); // 如果锁计数为0更新锁计数保持其对象
                rh.count++; // 读锁计数累加
                cachedHoldCounter = rh; // 保存读锁计数器对象
            }
            return 1; // 读锁加锁成功
        }
    }
}

【4】获取读锁的流程
读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下。

在这里插入图片描述

(2)lockInterruptibly方法

(3)tryLock方法

(4)unlock方法

在这里插入图片描述在这里插入图片描述
【1】tryReleaseShared方法
此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。

@ReservedStackAccess
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();// 获取当前线程对象
    if (firstReader == current) { // 当前线程是否是第一个持有锁线程
        if (firstReaderHoldCount == 1) // 是否是第一次上锁后就解锁了
            firstReader = null; // 清除第一个读锁线程
        else
            firstReaderHoldCount--;// 将读锁计数减一
    } else {
        HoldCounter rh = cachedHoldCounter; // 得到缓存的计数器对象
        if (rh == null || rh.tid != LockSupport.getThreadId(current))
            rh = readHolds.get(); // 如果缓存的计数器对象不是当前线程的,则获取当前线程的计数器对象,重新赋值
        int count = rh.count; // 得到当前线程的读锁计数
        if (count <= 1) { // 释放锁后为0,或者过度释放,则移除计数器
            readHolds.remove();// 移除计数器
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count; // 锁计数减1
    }
    for (; ; ) {
        int c = getState(); // 获得锁计数
        int nextc = c - SHARED_UNIT; // 读锁计数减一
        if (compareAndSetState(c, nextc)) // cas操作更新state值
            // 释放读取锁定对读取器没有影响.但是,如果现在读和写锁都已释放,则可能允许等待的编写器继续进行.
            return nextc == 0;
    }
}

流程图如下:
在这里插入图片描述
在读锁的获取、释放过程中,总是会有一个对象存在着,同时该对象在获取线程获取读锁是+1,释放读锁时-1,该对象就是HoldCounter。

要明白HoldCounter就要先明白读锁。前面提过读锁的内在实现机制就是共享锁,对于共享锁其实我们可以稍微的认为它不是一个锁的概念,它更加像一个计数器的概念。一次共享锁操作就相当于一次计数器的操作,获取共享锁计数器+1,释放共享锁计数器-1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。

(二)读写锁ReentrantReadWriteLock使用案例

(1)案例一:对缓存token的读写

第三方接口调用需要带上访问凭据accessToken ,需要先调用login接口传入username password获取该token。每个业务请求之前都login获取新的accessToken ,因为accessToken 不是一次性的,是有有效时间的。所以使用 ReadWriteLock 对工具类的进行了改造。

减少调用login接口的次数,无须每次调用业务接口都获取一次token;那么Token就得缓存起来,进而不同线程独读写共享变量就有并发问题:例如,读取缓存token时,无需加锁,各个线程之间的读是并发的互不影响;但是当token失效时,就必须重新申请,这时就得刷新token的缓存,就涉及到了并发读写。所以,用到了ReadWriteLock。最终,线程之间并发读不受影响,但是同一时刻只有一个线程可以写,而且写的时候 不会有线程能读到token。还需要注意一个细节,有可能有多个线程同时触发了申请token的条件,但最终只会有一个会成功申请token.

package com.example.demo;
 
 
import java.util.Date;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * @author wang 
 * @date 2022/9/23 10:37
 */
public class ReadWriteLockDemo {
    private static final ReentrantReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
    private static final Lock READ_LOCK = READ_WRITE_LOCK.readLock();
    private static final Lock WRITE_LOCK = READ_WRITE_LOCK.writeLock();
 
    //初始化Token; 这里一般是spring 容器舒适化过程中,先发一个请求获取token
    private AccessToken accessToken = new AccessToken(UUID.randomUUID().toString(), new Date());
 
    static   int i = 0;
 
    private void login(AccessToken expireToken) {
 
        WRITE_LOCK.lock();
        try {
            //如果this.accessToken 的值依然是 过期请求的值,才重新申请
            if (expireToken.equals(this.accessToken)) {
                //模拟http请求 获取 accessToken
                System.out.println(Thread.currentThread().getName() + "  申请了新的Token");
                //可以观察到 token申请期间 不会有doGet return相关数据
                Thread.sleep(1000);
                this.accessToken = new AccessToken(UUID.randomUUID().toString(), new Date());
            } else {
                System.out.println(Thread.currentThread().getName() + "其他线程已经更新过accessToken , 跳过申请token ");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            WRITE_LOCK.unlock();
        }
    }
 
    /**
     * 业务请求,需要用到 Token
     */
    public void doGet() throws InterruptedException {
        boolean isExpire;
        READ_LOCK.lock();
        try {
            //携带token 发起业务请求;由于不关注业务请求的返回值,只关注请求返回中 是否提示token失效;下面这行就代表发起业务请求
            isExpire = accessToken.isExpire();
            //如果token失效,需要重新登录
        } finally {
            READ_LOCK.unlock();
        }
 
        if (isExpire) {
            System.out.println(Thread.currentThread().getName() + "  token 过期 重新申请");
            login(this.accessToken);
            //重新发送一次业务请求
            doGet();
        } else {
            System.out.println(Thread.currentThread().getName() + "  业务请求成功! return 相关数据");
            i++;
        }
 
    }
 
    public static void main(String[] args) throws InterruptedException {
        ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    readWriteLockDemo.doGet();
                    Thread.sleep(new Random().nextInt(3));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }).start();
            Thread.sleep(500);
        }
        System.out.println(i);
    }
}
package com.example.demo;
 
import java.time.LocalDateTime;
import java.util.Date;
 
/**
 * @author wang 
 * @date 2022/9/23 11:14
 */
 
public class AccessToken {
    public AccessToken(String value, Date createTime) {
        this.value = value;
        this.createTime = createTime;
    }
 
    private String value;
    private Date createTime;
 
    public boolean isExpire() {
 
        try {
            //模拟业务请求响应时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //假设token有效期是 2 秒;  一般情况下,是发送业务请求之后 返回提示 token是否失效;
        return (System.currentTimeMillis() / 1000) - (createTime.getTime() / 1000) > 2;
    }
 
    public String getValue() {
        return value;
    }
 
    public void setValue(String value) {
        this.value = value;
    }
}

(2)案例二:测试

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class T01_ReadWriteLock {

    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public void read(Lock lock) throws InterruptedException {
        lock.lock();
        Thread.sleep(1000);
        System.out.println("read");
        lock.unlock();
    }

    public void write(Lock lock) throws InterruptedException {
        lock.lock();
        Thread.sleep(1000);
        System.out.println("write");
        lock.unlock();
    }

    public static void main(String[] args) {
        T01_ReadWriteLock t = new T01_ReadWriteLock();
        Runnable read = ()-> {
            try {
                t.read(readLock);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Runnable write = ()-> {
            try {
                t.write(writeLock);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for(int i = 0;i<20;i++) {
            new Thread(read).start();
        }

        for (int i = 0;i<3;i++) {
            new Thread(write).start();
        }
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/35013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GPS+北斗定位借助Arduino的数值显示

GPS北斗定位借助Arduino的数值显示 一、前言二、硬件要求三、参数基础四、原理剖析五、使用方式六、程序概要七、成果展示八、数据解析九、总结一、前言 较多的导航软件在生活中层出不穷&#xff0c;但是卫星定位同样也适用于轨迹记录、经纬分析、授时系统等&#xff0c;多样的…

【杰理AC696X】外挂FLASH音乐播放及Bin文件制作

外挂FLASH音乐播放及Bin文件制作 测试SDK版本&#xff1a;《ac696n_soundbox_sdk_v1.6.0》 文章目录外挂FLASH音乐播放及Bin文件制作前言一、板级配置1.1 SPI配置1.2 FLASH配置二、FLASH音乐播放测试三、FLASH BIN文件制作1.1 FLASHPCWinHex1.2 U盘分区工具WinHex1.3 FLASHPCF…

嵌入式单片机智能药盒设计(含代码)

目录 前言 设计的内容 &#xff08;1&#xff09;显示 &#xff08;2&#xff09;定时时间与用药量的设定 &#xff08;3&#xff09;实时时间调节 &#xff08;4&#xff09;报警功能 时钟模块 蓝牙模块 系统软件设计 系统主程序 按键函数 中断服务函数 作品展示 测试药…

2023年天津农学院专升本专业课参考教材

2023年天津农学院高职升本科专业课参考教材一、人力资源管理专业 1、《人力资源管理实用教程》 (第2版)&#xff0c;吴宝华&#xff0c;北京大学出版社 2、《人力资源管理》&#xff08;第4版&#xff09;&#xff0c;刘昕&#xff0c;中国人民大学出版社 3、《人力资源管理概论…

kobject 与sysfs属性文件读写

kobject和kset的简单总结 • kobject是struct kobject类型的对象。Kobject 有一个名字和一个引用计数。kobject 也有一个父指针&#xff08;允许 kobjects 被安排到层次结构中&#xff09;&#xff0c;一个特定的类型&#xff0c;也许还有一个在 sysfs 虚拟文件系统中的表示。…

OceanBase 4.0 解读:分布式查询性能提升,我们是如何思考的?

关于作者 王国平 OceanBase高级技术专家 目前OceanBase SQL引擎的技术负责人。2016年加入 OceanBase&#xff0c;负责SQL引擎的研发工作。2008年毕业于哈尔滨工业大学&#xff0c;2014年在新加坡国立大学获得博士学位&#xff0c;博士期间主要研究方向是数据库领域的(多)查询…

C++入门(2)-类与对象

类与对象初步认识类与对象一、面向过程与面向对象的区别二、类与结构体三、类的定义四、类的实例化五、类对象六、this指针七、构造函数八、析构函数九、拷贝构造函数十、运算符重载函数初步认识类与对象 一、面向过程与面向对象的区别 C语言是面向过程进行编程&#xff0c;注…

springboot整合其他项目

目录 一&#xff0c;集成Druid 学习地址&#xff1a; 主要讲监控 基于springboot视图渲染技术上增加代码 1.1 增加pom依赖 1.2 先在application.yml中添加Druid的数据源 1.3 其次在全局配置文件application.yml中添加所需的配置 配置截图&#xff1a; 配置解析 1.4 启动…

电脑录屏快捷键是什么?win10自带屏幕录制在哪

​在使用电脑的过程中&#xff0c;我们难免会遇到使用电脑录屏功能。有时候可能是想录制网课&#xff0c;有时候可能是想录制游戏的精彩操作&#xff0c;有时候可能只是想录制会议内容。 电脑录屏能够将重要的画面内容进行录制&#xff0c;十分的方便。但也有很多的小伙伴不清…

Python基础(三):PyCharm安装和使用

文章目录 PyCharm安装和使用 一、PyCharm的作用 二、PyCharm系统要求 三、下载和安装 四、PyCharm基本使用 五、PyCharm的基本设置 1、修改主题 2、修改代码文字格式 3、修改解释器 4、项目管理 PyCharm安装和使用 14天学习训练营导师课程&#xff1a;杨鑫《Python…

一、VSCode——免安装

介绍 Visual Studio Code支持可移植模式。此模式使 VS Code 创建和维护的所有数据都位于自身附近&#xff0c;因此可以跨环境移动。 此模式还提供了设置 VS Code 扩展的安装文件夹位置的方法&#xff0c;这对于阻止在 Windows AppData 文件夹中安装扩展的企业环境非常有用。 …

极市打榜|70G+已标注数据集出炉,油品泄露识别等全新算法上线!

极市打榜 算法打榜是极市平台推出的一种算法项目合作模式&#xff0c;至今已上线 100 产业端落地算法项目&#xff0c;已对接智慧城市、智慧工地、明厨亮灶等多个行业真实需求&#xff0c;算法方向涵盖目标检测、行为识别、图像分割、视频理解、目标跟踪、OCR等。 开发者报名…

CAN 协议控制器和物理总线之间的接口芯片SIT1040T 高速 CAN 总线收发器

CAN 协议控制器和物理总线之间的接口芯片SIT1040T 高速 CAN 总线收发器 CAN是最新进的现场总线,灵活性好,通讯可靠性高,抗干扰能力强&#xff0c;超长通信距离等优点,110个节点,兼带CAN-FD功能产品,容错电压可达-70V~ 70V,温度范围高达-40C ~ 150C最初应用于汽车电子,目前已广…

安信可Ai-WB1系列AT指令连接MQTT阿里云物联网平台

文章目录前言1 准备材料2 创建云端设备3 硬件连接4 配置终端设备5 MQTT实现发布&订阅消息联系我们前言 本文将介绍安信可AI-WB1系列通过AT指令接入阿里云物联网平台&#xff0c;实现MQTT的订阅和发布。 1 准备材料 AI-WB1系列模组或者开发板USB转TTL模块/Type-C数据线阿…

数仓建设教程

50000字&#xff0c;数仓建设保姆级教程&#xff0c;离线和实时一网打尽(理论实战) 上 - 腾讯云开发者社区-腾讯云 (tencent.com)50000字&#xff0c;数仓建设保姆级教程&#xff0c;离线和实时一网打尽(理论实战) 下_五分钟学大数据的技术博客_51CTO博客#yyds干货盘点#最强最全…

什么是DDoS攻击?企业服务器是否需要DDoS防御

有时候你可能会遇到某个网站突然打不开&#xff0c;这一段时间后发布自己被DDos攻击的公告&#xff0c; 那么&#xff0c;为什么DDOS攻击能让服务器瘫痪&#xff1f;黑客又如何执行DDos攻击的呢&#xff1f; DDoS全称为Distributed Denial of Service&#xff08;分布式拒绝服…

Anemoi hash:一种SNARK-friendly的哈希函数

随着zk的兴起&#xff0c;出现了一大批zk友好且面向算术化(Arithmetization-Oriented)的哈希函数&#xff0c;如MiMC-Hash, Rescue–Prime, Poseidon等等&#xff0c;本文要介绍的Anemoi是今年新出的一种zk友好且面向算术化的哈希函数&#xff0c;与其他哈希函数相比&#xff0…

让我们进入面向对象的世界(三)

文章目录前言一.了解什么是继承二.我们针对继承来设计一个动物继承树前言2.1 第一步 找出共同属性和行为的对象2.2 设计代表共同状态行为的类2.3 决定子类是否让某项行为有不同的运作方式。2.4我们仔细去观察一下子类的特征&#xff0c;争取更多的抽象化的机会。三.继承的相关语…

leetcode:887. 鸡蛋掉落【经典dp定义】

目录题目截图题目分析ac code总结题目截图 题目分析 变量&#xff1a;鸡蛋的数量&#xff0c;楼层n&#xff0c;尝试的次数m有一个单调性容易发现&#xff1a;尝试的次数越多&#xff0c;能解决楼层越高的确切值另一个单调性&#xff1a;鸡蛋的数量越多&#xff0c;能够解决楼…

Flutter 实现局部刷新 StreamBuilder 实例详解

一、前言 在flutter项目中&#xff0c;页面内直接调用setState方法会使得页面重新执行build方法&#xff0c;导致内部组件被全量刷新&#xff0c;造成不必要的性能消耗。出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新的操作。包括Provider、Va…