前言

很多时候,我们为了保证线程安全,会对一段代码加锁,但是加锁就意味着程序效率的下降,所以,我们经常会对锁进行一些优化,例如严格控制加锁的粒度,利用cas来代替加锁等。而今天我们介绍的读写锁,也是对锁的一种优化方案的实现。试想一下,如果我们的线程大部分时候都是读操作,那么读操作与读操作直接有必要互斥吗?答案是没有必要的,只有读写操作,写写操作才需要通过互斥来保证线程安全。今天我们通过ReentrantReadWriteLock来看看读写锁是如何实现的。

ReentrantReadWriteLock的简单使用

public void test1(){
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        new Thread(()->{
            Lock readLock = readWriteLock.readLock();
            readLock.lock();
            try{
                //读操作
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                readLock.unlock();
            }
        }).start();
        new Thread(()->{
            Lock writeLock = readWriteLock.writeLock();
            writeLock.lock();
            try{
                //写操作
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                writeLock.unlock();
            }
        }).start();
    }

readLock源码分析

首先是lock()方法。

lock()

readLock是通过共享锁来实现的。lock()方法会调用acquireShared()方法。所以我们直接分析acquireShared()方法。

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

acquireShared()

acquireShared()主要的逻辑就在tryAcquireShared()方法和doAcquireShared()方法,首先调用tryAcquireShared()方法,如果返回的值小于0,那么就需要调用doAcquireShared()方法进行阻塞,否则就会直接去执行业务代码。接下来我们重点分析tryAcquireShared()方法和doAcquireShared()方法。

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

tryAcquireShared()

这个方法的逻辑其实比较简单。可以分为三个部分来看

1.首先通过state(state的高16位表示读锁的获取次数,低16位表示写锁的次数)获取到写锁的count,会判断写锁的count是否大于0(大于0意味着有线程获取了写锁)并且获取写锁的线程不是当前线程,那么返回-1,需要执行doAcquireShared()方法去阻塞当前线程。

2.通过cas修改读锁的count(保证线程安全)。如果当前线程是第一个获取读锁的线程,那么修改firstReader(第一个获取读锁的线程),firstReaderHoldCount(第一个获取读锁的线程的重入次数),否则通过当前线程获取读锁的次数构建成一个HoldCounter对象,并且放入到readHolds中(readHolds是一个ThreadLocal),同时维护一个cachedHoldCounter的缓存(当前线程获取读锁的重入次数的缓存)。

3.cas失败,读锁的数量达到最大值或者readerShouldBlock()方法判断需要等待的话,就调用fullTryAcquireShared()方法。

        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);
            //readerShouldBlock()判断是否需要等待,如果不需要等待并且当前获取读锁的数量小于最大值的限制,cas也成功替换了读锁数量
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c   SHARED_UNIT)) {
                //如果当前读锁数量为0 那么当前线程就是第一个获取读锁的线程
                if (r == 0) {
                    //将当前线程赋值给firstReader 
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    //如果当前线程是第一个获取读锁的线程,那么只需要将firstReaderHoldCount  1 
                    //从这里我们就可以知道读锁是支持重入的
                    firstReaderHoldCount  ;
                } else {
                    //HoldCounter用来保存当前线程获取读锁的次数,因为读锁是支持重入的,
                    //readHolds是一个ThreadLocal,用来保存当前线程的HoldCounter
                    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;
            }
            //readerShouldBlock()返回true cas失败 或者已经达到最大数量
            return fullTryAcquireShared(current);
        }

fullTryAcquireShared()

tryAcquireShared()在cas失败的话,读锁数量达到最大值或者readerShouldBlock()方法判断需要等待的话就会进入fullTryAcquireShared()方法,而fullTryAcquireShared()方法方法就会分别针对这三种情况进行处理。

1.首先依旧会先判断是否有线程获取写锁,如果有,直接返回-1.

2.如果readerShouldBlock()方法返回true,如果返回true,表示应该阻塞等待,就将当前线程获取读锁的数量置为0,并且返回-1(返回-1就会调用doAcquireShared()方法去阻塞线程)。

3.如果读锁的数量已经达到最大值,那么就直接抛出异常

4.如果是因为cas失败,那么再进行cas一次,并且修改firstReader(第一个获取读锁的线程),firstReaderHoldCount(第一个获取读锁的线程的重入次数),cachedHoldCounter(当前线程获取读锁的重入次数的缓存)等变量的值。注意cas失败不会跳出for循环,所以这里是会自旋重试的。

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c   SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount  ;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count  ;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

接下来我们需要看一下readerShouldBlock()方法,readerShouldBlock()方法如果返回true那么代表线程获取读锁的时候需要阻塞,那我们就分析一下readerShouldBlock()方法,看什么时候获取读锁需要阻塞线程。

readerShouldBlock()

readerShouldBlock()方法有两个实现,一个是公平锁的实现,一个是非公平锁的实现。

公平锁实现:

判断阻塞队列中是否有节点,并且第一个节点不是当前线程。那么作为公平锁,这就代表需要等待。

final boolean readerShouldBlock() {
     return hasQueuedPredecessors();
}
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());
    }

非公平锁实现:

判断阻塞队列中的第一个节点是否不是共享节点,如果不是,那么就需要等待,否则就代表可以插队,也就不需要阻塞。

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

那么接下来我们分析doAcquireShared()方法,看doAcquireShared()方法是如何阻塞线程的。

doAcquireShared()

1.调用addWaiter()方法将当前线程封装成Node并且加入到阻塞队列中。

2.如果发现当前节点的前节点是head节点(代表当前节点是队列中第一个节点,因为head节点始终是空的,所以head的next就是实际上的第一个节点),那么就再次调用tryAcquireShared()方法尝试获取锁。如果调用tryAcquireShared()方法获取锁成功,那么就唤醒阻塞队列中所有的共享节点。

3.将阻塞队列中前一个节点的状态修改为SIGNAL,并且调用LockSupport.park()方法阻塞当前线程。

private void doAcquireShared(int arg) {
        //将线程封装成状态为SHARED的Node节点,并且加入到阻塞队列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前一个节点
                final Node p = node.predecessor();
                if (p == head) {
                    //再次调用tryAcquireShared()方法尝试获取锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //获取锁成功 因为是共享锁,那么需要唤醒所有的共享节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //将前节点设置为SIGNAL状态 并且调用LockSupport.park()阻塞当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //如果失败 那么就将节点状态修改为
                cancelAcquire(node);
        }
    }

接下来我们重点分析setHeadAndPropagate(),shouldParkAfterFailedAcquire(), parkAndCheckInterrupt()三个方法。

注:如果你看过我的另一篇文章# CountDownLatch源码分析,那么相信你对这三个方法已经很熟悉了,因为CountDownLatch也是通过AQS共享锁来实现的。

setHeadAndPropagate()

1.重新设置头节点,并且如果阻塞队列中的下一个节点是共享节点,那么就需要调用doReleaseShared()方法尝试去唤醒阻塞队列中其他的共享节点。doReleaseShared()方法我们在分析unlock()的时候再详细分析。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
}

shouldParkAfterFailedAcquire()

shouldParkAfterFailedAcquire()方法的作用就是将前面一个节点的状态修改为SIGNAL状态,并且将CANCELLED状态的节点去除(waitStatus大于0,只能是CANCELLED状态)。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        //如果前节点的状态是CANCELLED状态,那么尝试去除阻塞队列中的其他的CANCELLED状态的节点(注意这里只会从后往前遍历吗,去除连续的CANCELLED状态的节点)
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //cas修改前一个节点的状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()

parkAndCheckInterrupt()方法会调用LockSupport.park()方法,是真正阻塞线程的地方。线程被唤醒后,会从这个地方继续执行代码。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

至此readLock的lock()方法,我们就分析完了。接下来我们继续分析readLock的unlock()方法。

unlock()

unlock()方法的逻辑都是在releaseShared()方法中完成的,所以我们具体看releaseShared()方法。

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

releaseShared()

releaseShared()方法先调用tryReleaseShared()方法尝试是否锁,如果返回true,那么调用doReleaseShared()方法释放锁。

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

tryReleaseShared()

tryReleaseShared()方法的逻辑主要分为以下几步。

1.首先判断当前线程是否是第一个获取读锁的线程,如果是,那么就维护firstReader和firstReaderHoldCount变量,否则读取缓存cachedHoldCounter中的值,如果缓存的不是当前线程的值,那么就需要从readHolds中获取到当前线程的HoldCounter对象(保存了当前线程的重入次数),将当前线程的重入次数-1。

2.通过cas和自旋将获取总的读锁的数量-1,减完之后当前占有读锁的数量为0,那么就返回true。

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;
            }
        }

tryReleaseShared()方法返回true之后,需要调用doReleaseShared()方法唤醒被阻塞的线程。

doReleaseShared()

doReleaseShared()方法需要唤醒阻塞队列中所有的共享节点,通过自旋和unparkSuccessor()方法不断尝试唤醒阻塞队列中的节点。

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果头结点的状态是SIGNAL
                if (ws == Node.SIGNAL) {
                    //cas修改节点的状态为0 失败的话继续自旋
                    // 成功的话调用unparkSuccessor唤醒头结点的下一个正常节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //如果节点状态为0 那么cas替换为PROPAGATE 失败进入下一次自旋
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

unparkSuccessor()

unparkSuccessor()方法的作用是唤醒头节点后第一个不为null且状态不为cancelled的节点。通过LockSupport.unpark()方法唤醒阻塞的线程。

private void unparkSuccessor(Node node) {
        //获取头结点的状态 将头结点状态设置为0 代表现在正在有线程被唤醒 如果head状态为0 就不会进入这个方法了
        int ws = node.waitStatus;
        if (ws < 0)
            //将头结点状态设置为0
            compareAndSetWaitStatus(node, ws, 0);
	//唤醒头结点的下一个状态不是cancelled的节点 (因为头结点是不存储阻塞线程的)
        Node s = node.next;
	//当前节点是null 或者是cancelled状态
        if (s == null || s.waitStatus > 0) {
            s = null;
	 //从aqs链表的尾部开始遍历 找到离头结点最近的 不为空的 状态不是cancelled的节点 赋值给s 
         //这里为什么从尾结点开始遍历而不是头结点 是因为添加结点的时候是先初始化结点的prev的, 从尾结点开始遍历 不会出现prve没有赋值的情况 
         //如果从头结点进行遍历 next为null 并不能保证链表遍历完了
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
	    //调用LockSupport.unpark()唤醒指定的线程
            LockSupport.unpark(s.thread);
    }	

至此readLock的unlock()方法也就分析完了。

小结

1.ReentrantReadWriteLock的readLock是支持重入的。

2.ReentrantReadWriteLock的readLock是通过AQS的共享锁来实现的。

3.readLock中提供了firstReader,firstReaderHoldCount,cachedHoldCounter等变量来提供效率,当前线程的读锁重入次数一般都是存放在readHolds中(readHolds是一个TheadLocal),只有一个获取读锁的线程是通过firstReader,firstReaderHoldCount两个变量来维护的,而cachedHoldCounter则是一个缓存,这样通过这三个变量就可以减少从readHolds获取值的次数。(因为大部分情况下并发不高或许只有一个线程获取读锁)。

4.读锁与读锁之间不是互斥的,读锁和写锁是互斥的,但是如果获取写锁的线程是当前线程,那么当前线程是可以获取读锁的。

以上就是java线程安全锁ReentrantReadWriteLock原理分析readLock的详细内容,更多关于java锁ReentrantReadWriteLock readLock的资料请关注Devmax其它相关文章!

java线程安全锁ReentrantReadWriteLock原理分析readLock的更多相关文章

  1. ios – Realm Swift模型是否分开?

    我是iOS和Swift的世界的新手,正在开发一个新的应用程序,我想使用领域来保持持久性.我的代码中已经有我的服务访问和填充HTTPAPI端点的实体.现在我想坚持一些实体,并且想要建议我是否应该为每个实体创建新的领域特定的模型,以便从领域读写.或者我应该将我现有的所有简单的Swift实体转换为国家实体.起初,我感觉到错误,因为我将在我的应用程序中传递RealmEntities,而不是在持久层中.但是

  2. 在Swift中应用Grand Central Dispatch(上

    在这两篇教程中,你会学到GCD的来龙去脉。起步libdispatch是Apple所提供的在IOS和OSX上进行并发编程的库,而GCD正是它市场化的名字。Swift中的闭包和OC中的块类似甚至于他们几乎就是可交换使用的。但OC中的块可以安全的替换成Swift中的闭包。再一次,这完全取决于GCD。QoS等级表示了提交任务的意图,使得GCD可以决定如何制定优先级。QOS_CLASS_USER_INteraCTIVE:userinteractive等级表示任务需要被立即执行以提供好的用户体验。

  3. 【荐】Grand Central Dispatch Tutorial for Swift: Part 1/2

    所有的dispatchqueues自身都是线程安全的。dispatch_sync把任务添加到对应队列并等待其完成后再继续执行当前任务,容易造成死锁,或阻塞当前任务。dispatch_after指定时间后把任务添加到队列中。效果就像是延时后的dispatch_async。而array和dictionary在swift中是以struct的形式实现的,所以以上的读操作返回的是一个副本。在GCD中使用dispatchbarrier来解决这个问题。dispatchbarrier是一组方法,它们都已顺序化的方式来结合

  4. swift详解之十六-----------GCD基础部分

    当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。这完全取决于GCD。这个队列就是用于发生消息给UIView或发送通知的。GCD的“艺术”归结为选择合适的队列来调度函数以提交你的工作。

  5. Swift多种实现单例写法

    学习Swift的单例写法,以下提供了5种方式,一起来看看吧!

  6. Swift线程安全详解-概念,三种锁,死锁,Atomic,synchronized

    研究了下,是线程安全问题。UIKit以及Fundation事实上,大多数Cocoa提供的Api都不是线程安全的,尤其是与UI相关的UIKit,只能在主线程上操作。需要线程安全的时候,开发者自己维护就可以了。用来加锁,解锁。关于ObjectiveC参考这篇文章Atomic一个非Atomic的属性在非ARC的时候像这样可以看到,如果在多线程同时set的情况下,可能会造成release两次。Property的Runtime对应的C代码为可以看到,如果是nonatomic的,synchronized可以看看这个S

  7. swift - singleton

    关于单例,有三个重要的准则需要牢记:单例必须是唯一的,在程序生命周期中只能存在一个这样的实例。单例的存在使我们可以全局访问状态。为保证单例的唯一性,单例类的初始化方法必须是私有的。这样就可以避免其他对象通过单例类创建额外的实例。通过调用dispatch_once,即可保证实例化代码只运行一次。

  8. Swift线程安全的单例实现

    介绍单例模式在开发中尤为常见,最大的好处就是内存中只会保留一个对象,节省内存开销,同时便于对象的建立和维护。一个简单的单例这个单例,如此的简单那么他是线程安全的吗?答案是—————是!原因很简单,let定义的属性本身就是threadsafe的,同时static定义的是一个classconstant,拥有全局作用域和懒加载特性。如果你是Swift1.2以下的你可以这么写用GCD来实现线程安全这个实现大家应该相当熟悉,这里就不再过多介绍了。

  9. Swift 线程安全数组

    有并发的地方就存在线程安全问题,尤其是对于Swift这种还没有内置并发支持的语言来说线程安全问题更为突出。下面我们通过常见的数组操作来分析其中存在的线程问题,以及如何实现一个线程安全数组。因为对于值类型来说Swift采用的是copyOnWrite机制,所以在进行copyOnWrite处理是可能数组已经被另一个写操作给修改了。

  10. swift中的线程安全单例

    我有和应用程序有一个单一的存储整个应用程序的信息.但是,当使用来自不同线程的单例时,这会产生一些数据争用问题.这里有一个非常虚拟和简单化的问题版本:独生子单例的使用有没有办法确保单例是线程安全的,所以它可以在应用程序的任何地方使用,而不必担心你在哪个线程?

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部