JUC中的锁

2019-08-16 11:14:02来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

JUC中的锁

★、不同角度的锁的理解:

#1、公平锁、非公平锁

  公平锁:eg: ReentrantLock

    关键词:先来先服务。 加锁前检查是否有排队等锁的线程,若有,当前线程参与排队,先排的线程优先获取锁。相对没有  非公平锁  效率高。  

  非公平锁:eg:Synchronized   ReentrantLock

    与公平锁相反,不考虑排队问题,一旦资源释放,不考虑线程先来还是后到,所有线程统一进行资源抢占。【相对于已经先来很久的线程不公平】,非公平锁可能发生 线程饥饿 现象。

# 2、独享锁、共享锁:(广义上)

  独享锁:该锁一次只能被一个线程所持有。eg:ReentrantLock、读写锁ReentrantReadWriteLock的写锁、Synchronized

  共享锁:该锁可被多个线程所持有。           eg:读写锁ReentrantReadWriteLock的读锁

#3、互斥锁、读写锁【 #2、独享锁、共享锁:(广义上) 的具体实现 】

  互斥锁:ReentrantLock

  读写锁:ReentrantReadWriteLock

#4、重入锁  ,eg:Synchronized、 ReenTrantLock 【https://www.cnblogs.com/qifengshi/p/6831055.html

  可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁 。一定程度上,可以避免死锁。

#5、乐观锁、悲观锁 [ 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度 ]https://www.cnblogs.com/qifengshi/p/6831055.html

  乐观锁: 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

  悲观锁: 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

   悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
   悲观锁在Java中的使用,就是利用各种锁。
   乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

#6、 分段锁、【分段锁其实是一种锁的设计,并不是具体的一种锁https://www.cnblogs.com/qifengshi/p/6831055.html

   典型案例: ConcurrentHashMap的底层实现。

     我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

 

#7、 自旋锁

  线程发现所资源已经被占用时,不会立即阻塞,而是进行自旋,等待资源是否会被释放,自旋过程占用处理器时间,但避免了线程切换的开销。所以,锁占用时间越短越适合自旋锁,否则浪费处理器资源。

#8、 偏向锁、轻量级锁、重量级锁   ----->>>  从低到高锁膨胀

   偏向锁:数据在无竞争情况下,消除同步,连轻量级锁的CAS操作都省略,进行性能优化。偏向于第一个获取锁的线程,再接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的当前线程永不需要同步

      对象头中锁标志位为01意为偏向锁,直到一个新线程获取这个锁,偏向模式结束。

  轻量级锁:没有多线程竞争的前提下,优化重量级锁产生的性能消耗。对象头锁标志位00,表示轻量级锁。

    重量级锁:指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。

         重量级锁会让其他申请的线程进入阻塞,性能降低。

 

★、java中具体锁的源码分析

重入锁:  ReentrantLock

       @  支持一个线程对资源重复加锁,调用lock()时,已经获取到锁的线程,能够再次调用lock()获取锁而不被阻塞。

  @  支持获取锁时的公平与非公平选择【通过构造函数实现,如下图ReentrantLock.class源码】。默认实现的是     非公平锁。

                                  ps: 因为刚释放资源锁的线程再次获取的几率较大,所以会出现一个线程连续获取锁的现象,使得其他线程出现饥饿现象。如此一来,为何还是默认为非公平锁?

            原因是非公平锁切换次数少,开销更小,保证了更大的吞吐量。公平锁正好相反。

       

  如果实现的是公平锁,创建一个内部类FairSync对象,若是不公平锁,创建一个内部类NonfairSync对象。这两个类都是ReentrantLock的静态内部类,二者都继承自ReentrantLock的另一个静态内部类Sync。

  Sync 继承队列同步器AQS【AbstractQueuedSynchronizer】。由此可见,ReentrantLock就是一个自定义同步组件,Syc继承AQS,以静态内部类形式存在于ReentrantLock,同时为了扩展Syc,两个子类FairSync和NonfairSync实现公平与非公平。

    AQS中的同步状态volatile变量state 在ReentrantLock中表达的意思就是锁被一个线程获取的次数。

    

Sync 源码
  
  abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; abstract void lock(); 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; } 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; } }

  Syc要重写AQS的tryAcquire和tryRelease方法。因为分为两种情况,于是

    1、公平锁的重写tryAcquire方法在FairSync中,方法名为tryAcquire,在其内实现逻辑。

    2、非公平锁重写tryAcquire方法在NonfairSync中,方法名为tryAcquire ,直接调用  实现逻辑在Syc父类的nonfairTryAcquire方法。

    3、tryRelease方法的重写没有分开,重写在Syc父类中。

  1、上述Sync类源码中的nonfairTryAcquire方法。 c = getState(),获取到当前资源所得状态。如果c = 0,说明当前没有线程占有锁。那么就用CAS原子操作,更改锁状态为1,表示锁资源已经被占有。通过setExclusiveOwnerThread方法标识当前线程在占有锁。

  如果c != 0,则表示当前锁资源已经被占有,先判断当前线程是否为已经占有锁的线程,如果不是,返回false,阻塞线程。如果是,将锁标志状态state继续累加,返回true,表示同步状态成功,以实现可重入的控制。

  2、再看一下公平锁重写的tryAcquire方法的部分代码:和非公平锁的nonfairTryAcquire方法对比,重入控制部分一模一样,整体上唯一不同的就是在初次获取锁资源的时候不同。判断条件多了一个

    hasQueuedPredecessors()方法【AQS中】。用于判断 加入了同步队列中的当前节点是否有前驱节点,若返回true,表示有,说明当前线程之前已经有线程在排队等待获取锁了,那么当前线程也要排队等待。

              

  3、再看一下Sync类源码中的tryRelease()方法。一开始就   int c = getState() - releases;然后判断当前线程是否为之前记录的获取锁资源的线程,如果不是不合逻辑,抛出异常。还是因为涉及重入的原因,如果一个

  线程获取了n次,那么前 n-1 次释放,都要返回等于false。直到最后一次释放,c=0,那么 setExclusiveOwnerThread(null);表示当前没有线程获取资源,线程记录为null,setState(c);让锁标志状态恢复为0表示锁资源

       未被占用。最后返回true表示资源释放成功。

 

读写锁: ReentrantReadWriteLock

  维护了一个读锁,一个写锁。同一时刻允许多个读 线程访问,当写线程访问时,后续所有读写线程均被阻塞。相对于排他锁,提升了并发性和吞吐量。

  @支持公平和非公平(默认)两种锁获取方式

  @支持可重入,eg:读锁可重入获取读锁, 写锁可重入获取读锁或写锁

  @锁降级,获取写锁--》获取读锁--》释放写锁次序,写锁可以降为读锁。

  类ReentrantReadWriteLock内部分源码截取:

    private final ReentrantReadWriteLock.ReadLock readerLock;     //定义一个静态内部类读锁
    private final ReentrantReadWriteLock.WriteLock writerLock;  //定义一个静态内部类写锁

   

   //两个构造方法,创建公平、非公平锁  类似ReentrantLock  ,默认非公平锁

   public ReentrantReadWriteLock() {

    this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//创建公平、非公平锁Sync,
        readerLock = new ReadLock(this);//构造方法中创建读锁,同时把sync通过构造方法注入ReadLock
        writerLock = new WriteLock(this);//构造方法中创建写锁,同时把sync通过构造方法注入WriteLock
  }
  
    
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }//
实现ReadWriteLock接口的writeLock方法,用于获取写锁
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }//实现ReadWriteLock接口的readLock方法,用于获取读锁
    /**
     * The lock returned by method {@link ReentrantReadWriteLock#readLock}.
     * 实现Lock接口的读锁,作为ReentrantReadWriteLock的静态内部类
     */
    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync; protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } public void lock() { sync.acquireShared(1);//把读锁对象和sync关联起来,而且acquireShared表明读锁是共享锁 } public void lockInterruptibly() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public boolean tryLock() { return sync.tryReadLock(); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } public void unlock() { sync.releaseShared(1); } public Condition newCondition() { throw new UnsupportedOperationException(); } public String toString() { int r = sync.getReadLockCount(); return super.toString() + "[Read locks = " + r + "]"; } } /** * The lock returned by method {@link ReentrantReadWriteLock#writeLock}. * 实现Lock接口的写锁,作为ReentrantReadWriteLock的静态内部类 */ public static class WriteLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -4992448646407690164L; private final Sync sync; protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } public void lock() { sync.acquire(1);//这一行把写锁和创建的sync关联起来了,而且调用的 acquire方法表名写锁是排他锁 } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock( ) { return sync.tryWriteLock(); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public String toString() { Thread o = sync.getOwner(); return super.toString() + ((o == null) ? "[Unlocked]" : "[Locked by thread " + o.getName() + "]"); } public boolean isHeldByCurrentThread() { return sync.isHeldExclusively(); } public int getHoldCount() { return sync.getWriteHoldCount(); } }
   /**
     * 公平锁
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }

    /**
     * 非公平锁
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
   /**
     * 类似于ReentrantLock,Sync继承AQS作为自定义同步器,以静态内部类的形式存在于ReentrantReadWriteLock
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;


    //位运算 来实现 资源状态变来那个state同时控制标记读锁和写锁
static final int SHARED_SHIFT = 16;      //位运算移动的位数 static final int SHARED_UNIT = (1 << SHARED_SHIFT); //1 位运算左移16位得到的数值 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //1 位运算左移16位得到的数值再减去1 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Returns the number of shared holds represented in count */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //c /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); } static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }

private transient ThreadLocalHoldCounter readHolds; private transient HoldCounter cachedHoldCounter; private transient Thread firstReader = null; private transient int firstReaderHoldCount; Sync() { readHolds = new ThreadLocalHoldCounter(); setState(getState()); // ensures visibility of readHolds } abstract boolean readerShouldBlock(); abstract boolean writerShouldBlock();

    //重写AQS的tryRelease方法,主要用于ReentrantReadWriteLock的写锁【排他】
        protected final boolean tryRelease(int releases) {
           。。。。
        }
    //重写AQS的tryAquire方法,主要用于ReentrantReadWriteLock的写锁【排他】
    protected final boolean tryAcquire(int acquires) {
          ...
        }

//重写AQS的tryReleaseShared方法, 共享释放同步状态,
主要用于ReentrantReadWriteLock的读锁【共享】
      protected final boolean tryReleaseShared(int unused) {
           ...
        }

        private IllegalMonitorStateException unmatchedUnlockException() {
            return new IllegalMonitorStateException(
                "attempt to unlock read lock, not locked by current thread");
        }

    //重写AQS的tryAcquireShared方法,共享获取锁,主要用于ReentrantReadWriteLock的读锁【共享】
protected final int tryAcquireShared(int unused) { ... } }

 

  ps:先提一下读写锁关于锁状态的实现:

     AQS的volatile变量state在这里表示读写状态。【联系ReentrantLock中,state意思是锁被线程重复获取的次数】。鉴于读锁可重入共享,写锁排他独占,所以这个变量要维护多个读线程和一个写线程的状态。

      变量分割:高16位表示读,低16位表示写。

       

 

  上述源码中已经添加了对应注释便于理解,现再梳理一遍。

  @写锁的获取。先看下写锁WriteLock类中的lock方法代码:说明写锁是排他锁。

   public void lock() {
        sync.acquire(1);//这一行把写锁和创建的sync关联起来了,而且调用的 acquire方法表名写锁是  排他锁
    }

  与ReentrantLock不同,Syc重写的tryAcquire方法和tryRelease方法没有写在Sync的子类FairSync、NonfairSync中,而是全都重写在父类Sync中。代码如下:   

     protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();      //获取当前线程
            int c = getState();      //获取共享资源状态,可以理解为 读写状态
            int w = exclusiveCount(c);  //通过位运算获取写锁【独占锁资源】状态值
            if (c != 0) {          //说明此时已经有线程获取了锁
                // 如果w==0,说明写锁没有被获取,而c又不等于0,说明当前线程已经获取了读锁,直接return false,不允许当前线程再去获取写锁,原因在于读写锁要保证写锁的操作对读锁可见,若读锁在已经被获取的情况下允许写锁的获取,那么正在运行的其它线程就无法感知到当前获取写锁的写线程的操作
//如果w != 0,再去判断上次获取写锁的线程是否是当前线程,如果不是,说明当前线程无法获取已经被抢占的写锁,return false
if (w == 0 || current != getExclusiveOwnerThread()) return false;
//此处,意为:当前线程上次已经获取到了写锁,准备再次重入写锁
if (w + exclusiveCount(acquires) > MAX_COUNT)    //位运算修改状态 throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; }
//此处意为c==0,还没有任何锁被获取。 writeShouldBlock方法根据是否是公平所,来决定是否阻塞当前线程。
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current);//当前线程设为占有写锁的唯一线程 return true; }

   @写锁的释放  与ReentrantLock的释放过程类似。比较简单易于理解

 

 protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())        //判断当前线程是否等于  排他锁拥有者线程,返回false,说明逻辑就不对了,抛异常
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;  //写状态是否为0,说明写锁当前没有被抢占,全被释放
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

  @读锁的获取  同理,先看下读锁ReadLock中lock()方法代码,说明读锁是一个 共享锁;

  public void lock() {
    sync.acquireShared(1);    //  acquireShared方法表名写锁是 共享锁

  }

   与ReentrantLock不同,Syc重写的tryAcquireShared方法和tryReleaseShared方法没有写在Sync的子类FairSync、NonfairSync中,而是全都重写在父类Sync中。代码如下:

 protected final int tryAcquireShared(int unused) {
            
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)    //exclusive方法获取占有独占锁【写锁】的数值,如果不等于0(说明写锁已经被占有)而且当前线程不是上次占有写锁的线程, 意思即 当前线程作为一个新线程在已经有别的线程占有写锁的情况下,不允许获取读锁,进入等待状态
                return -1;
            int r = sharedCount(c);//获取【共享锁】读锁的数值
            if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {  // 可以获取读锁,,不被阻塞 &&  读锁的值 小于 ?【MAX_COUNT没理解】  && 成功原子更改状态
                if (r == 0) {            //此时还没有线程获取读锁
                    firstReader = current;     //  把当前线程作为第一个获取读锁的线程
                    firstReaderHoldCount = 1;   //获取读锁的计数器 +1
                } else if (firstReader == current) {
                    firstReaderHoldCount++;     //当前线程重入获取读锁   获取读锁计数器+1
                } else {
//ps:因为读状态是所有线程获取锁次数的总和,但是每个线程各自获取读锁次数只能保存在ThreadLocal中,由线程自己维护!!!

//当前已经有线程获取读锁,而且当前线程并不是第一个获取读锁的线程, HoldCounter rh
= cachedHoldCounter; //内部定义的线程记录缓存,HoldCounter类用来记录线程已经获取锁得数量 if (rh == null || rh.tid != getThreadId(current)) //如果不是当前线程 cachedHoldCounter = rh = readHolds.get(); //从每个线程的本地变量ThreadLocal中获取 else if (rh.count == 0) //如果记录为0初始值设置 readHolds.set(rh); //设置记录 rh.count++; //自增 } return 1; //表示获取到读锁同步状态 } return fullTryAcquireShared(current); //如果CAS设置失败,执行此方法 }
//这一段代码注释来自:https://www.cnblogs.com/wait-pigblog/p/9350569.html
final
int fullTryAcquireShared(Thread current) { //内部线程记录器 HoldCounter rh = null; for (;;) { int c = getState();//同步状态 if (exclusiveCount(c) != 0) {//代表存在独占锁 if (getExclusiveOwnerThread() != current)//获取独占锁的线程不是当前线程返回失败 return -1; } else if (readerShouldBlock()) {//判断读锁是否应该被阻塞 if (firstReader == current) { } else { if (rh == null) {//为null rh = cachedHoldCounter;//从缓存中进行获取 if (rh == null || rh.tid != current.getId()) { rh = readHolds.get();//获取线程内部计数状态 if (rh.count == 0) readHolds.remove();//移除 } } if (rh.count == 0)//如果内部计数为0代表获取失败 return -1; } } if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) {//CAS设置成功 if (sharedCount(c) == 0) { firstReader = current;//代表为第一个获取读锁 firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++;//重入锁 } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; //将当前多少读锁记录下来 } return 1;//返回获取同步状态成功 } } }

    @读锁的释放 

//这一段代码注释来自:https://www.cnblogs.com/wait-pigblog/p/9350569.html
protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();//获取当前线程
            if (firstReader == current) {//如果当前线程就是获取读锁的线程
                if (firstReaderHoldCount == 1)//如果此时获取资源为1
                    firstReader = null;//直接赋值null
                else
                    firstReaderHoldCount--;//否则计数器自减
            } else {
           //其他线程
                HoldCounter rh = cachedHoldCounter;//获取本地计数器
                if (rh == null || rh.tid != current.getId())
                    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;//代表已经全部释放
            }
        }
!!!只有当同步状态的值为0的时候也就是代表既没有读锁存在也没有写锁存在才代表完全释放了读锁。

 

  现在汇总下读写锁的释放和获取;

  写锁 独占锁可重入。

    当前已经有线程获取了读锁,不允许再获取写锁,进入阻塞等待。但是可以再次允许线程获取读锁。

              写锁独占,可重入,但同时只能有一个线程获取写锁

  读锁,共享锁可重入。

    没有任何线程获取写锁的情况下,读锁总可以成功被获取、重入。如果有任何线程已经获取了写锁,当前线程获取读锁失败,进入阻塞等待。

 

  @锁降级  当前线程拥有写锁的情况下,又获取到读锁,然后把写锁释放掉。

   之所以不能直接释放写锁,反而要先获取读锁,目的是为了保证数据可见性。如果直接释放写锁,可能一个新线程T立马获取写锁,进行数据修改,那么就对当前线程不可见。如果先获取了读锁,那么T就会被阻塞。

          读写锁ReentrantReadWriteLock不允许锁升级,同样是为了保证数据可见性。

 

 分段锁:Segment

 见另一篇文章  HashMap的家族  ConcurrentHashMap   https://www.cnblogs.com/dxxdsw/p/11278408.html


原文链接:https://www.cnblogs.com/dxxdsw/p/11249688.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Java入门第二季学习总结

下一篇:SpringBoot2.x开发前准备