ConcurrentHashMap底层源码解析

news2024/7/6 17:50:57

ConcurrentHashMap线程安全,底层数组+链表+红黑树。
思想,分而治之。

JDK7有Segment对象,分段锁
JDK8没有了这个对象

总结,
首先计算hash,
如果集合没有元素,开始initTable方法初始化,这里扩容讲,这里只是初始化扩容,不是重点,重点是真正多线程扩容那里。
否则找到数组下标元素进行判断,这里找到这个位置上的数组用到了unsafe类中方法保证可见性,如果这个位置为null,新建Node,这里新建用cas去修改,保证原子性。这样当有其他线程进入后,发现cas操作失败,就会跳出,再次循环判断

这样比方第一个线程先将值放入,第二个就不会走到这里,

如果有元素,判断是链表还是树
如果是链表,比较key是否相等,相等,赋值,不相等,new一个Node节点.这里有个binCount记录链表大小,然后判断binCount判断是否需要转红黑树
如果是TreeBin对象,对象里有TreeNode,感觉套了个壳子,包括整颗红黑树,这样加锁加在了TreeBin上,这里和hashmap不一样,hashmap里面就是TreeNode,hashmap不用加锁,线程不安全,所以不用再有个TreeBin对象。然后将这个元素放到红黑树中

初始化扩容

发现是空,开始初始化,tab = initTable();只有一个线程可以进行初始化
sizeCtl有多个情况,首先默认是0,cas操作先将其改成-1,-1表示有线程正在扩容,然后扩容sc=12,sizeCtl也变成域值12。这里如果另一个线程发现sizectl=-1,然后暂时放弃cpu资源,再去和其他线程去竞争cpu资源Thread.yield(); 如果一个线程初始化完成,其他线程发现已经初始化了,就退出了

这个方法是初始化扩容,只有一个线程可以,第一次扩容初始化sizeCtl变成12

private transient volatile int sizeCtl;
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //如果不是null,说明其他线程已经初始化好了,直接退出方法
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
            //另一个线程从主内存拿到的sizeCtl=-1,走到这里,暂时放弃cpu资源,再去和其他线程去竞争cpu资源
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            //第一次集合空,初始化扩容走这里,对sizeCtl进行cas做-1操作
                try {
                    if ((tab = table) == null || tab.length == 0) {
                    //这里第一次sc=0,n=16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //这里n-3/4n=1/4 n,就是0.75n=12
                        sc = n - (n >>> 2);
                    }
                } finally {
                //第一次扩容初始化sizeCtl变成12
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

put方法源码

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
            //初始化数组方法,这里只有一个线程可以初始化
                tab = initTable();
            //这里用到了Unsafe类中方法
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //这里用了cas去修改
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                             //其他线程如果失败,重新开始for循环
                    break;                   // no lock when adding to empty bin
            }
            //判断是否是-1,-1表示集合在进行扩容,两个线程同时进行扩容,
            //这里这个线程也进行扩容,这样扩容更快
            //这个方法帮助扩容
            else if ((fh = f.hash) == MOVED)
            //这里扩容时候会有一个ForwordingNode对象,这个对象hash就是MOVED=-1
            //put时候发现是forwordingNode对象,说明这个对象已经被转移到新数组上去了
            //调用help方法帮助转移元素
                tab = helpTransfer(tab, f);
            else {
            //这里说明该位置有元素,这里要判断到底是放到哪里,是红黑树还是链表
                V oldVal = null;
                //这里又涉及到并发问题,这里1.7用的分段锁,1.8用了synchronized
                //对链表里面第一个节点f进行加锁
                synchronized (f) {
                //判断是否==f,加锁过程可能有其他线程操作修改了,如果修改了,就不走这里了
                    if (tabAt(tab, i) == f) {
                    //fh >= 0,表示f是链表上面的节点
                        if (fh >= 0) {
                        //binCount=1
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //否则是树里面的一个节点
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                //binCount大于8,改成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //真正启动扩容,首先要统计size
        addCount(1L, binCount);
        return null;
    }

这里转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                    	//这里改成双向链表
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

这里计数
addCount方法,cas并发高效率也不行,所以用另外一个数组CounterCell[],分成了CountCell,对它进行+1操作。如果一个线程用不了cell,还是对baseCount操作
jdk1.8 版本中,对 ConcurrentHashMap 做了优化,取消了分段锁的设计,取而代之的是通过 cas 操作和 synchronized 关键字来实现优化,而扩容的时候也利用了一种分而治之的思想来提升扩容效率。
在 HashMap 中,调用 put 方法之后会通过 ++size 的方式来存储当前集合中元素的个数,但是在并发模式下,这种操作是不安全的,所以不能通过这种方式,那么是否可以通过 CAS 操作来修改 size 呢?

直接通过 CAS 操作来修改 size 是可行的,但是假如同时有非常多的线程要修改 size 操作,那么只会有一个线程能够替换成功,其他线程只能不断的尝试 CAS,这会影响到 ConcurrentHashMap 集合的性能,所以作者就想到了一个分而治之的思想来完成计数。
作者定义了一个数组来计数,而且这个用来计数的数组也能扩容,每次线程需要计数的时候,都通过随机的方式获取一个数组下标的位置进行操作,这样就可以尽可能的降低了锁的粒度,最后获取 size 时,则通过遍历数组来实现计数:

假如 CounterCell 为空且 CAS 失败,那么就会通过调用 fullAddCount 方法来对 CounterCell 数组进行初始化。

在这里插入图片描述
多个线程同时put,有个size属性,我们要通过多个线程给size这个属性+1.
这里出现并发,我们怎么控制安全,可以用cas控制,但是很多线程的话,cas效率不高。所以这里用到了一个重要思想,分而治之。
源码里又定义了一个CounterCell数组。如果cas操作baseCount成功,+1.但是还有很多线程是失败的,这些线程每个线程会生成一个随机数ThreadLocalRandom.getProbe()
然后计算ThreadLocalRandom.getProbe() & m,得到一个下标值,就像上面那张图,cell数组长度是4,然后每个线程通过计算得到下标后,还是利用cas,对CounterCell这个中的value进行+1操作,然后计数结束开始统计,遍历数组CounterCell个数加上baseCount个数就是最终的总个数。这样效率会快,这就是分而治之思想。

如果cell是null,初始化,如果另一个线程初始化失败,就会去cas操作baseCount,不成功就从一开始继续循环。
如果cell不是null,扩容

这里面有一个比较重要的变量 cellsBusy,默认是 0,表示当前没有线程在初始化或者扩容,所以这里判断如果 cellsBusy==0,而 as 其实在前面就是把全局变量 CounterCell 数组的赋值,这里之所以再判断一次就是再确认有没有其他线程修改过全局数组 CounterCell,所以条件满足的话就会通过 CAS 操作修改 cellsBusy 为 1,表示当前自己在初始化了,其他线程就不能同时进来初始化操作了。
最后可以看到,默认是一个长度为 2 的数组,也就是采用了 2 个数组位置进行存储当前 ConcurrentHashMap 的元素数量。

@sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }
x=1
private final void addCount(long x, int check) {
//默认CounterCell数组是null
        CounterCell[] as; long b, s;
        //第一次进来countcell是null,cas修改baseCount=0+1=1,修改成功,不会走这里,走下面方法,和CounterCell数组没关系了,如果修改失败,false,取反是true。走进来了
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            //
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //ThreadLocalRandom.getProbe() & m算出数组下标,
            //如果as是空的或者计算出的下标值是null或者cas操作失败
            //cas是对CounterCell中的value属性进行+1操作,如果没有成功也是调用fullAddCount
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                  //假如 CounterCell 为空且 CAS 失败,那么就会通过调用 fullAddCount 方法来对 CounterCell 数组进行初始化及扩容。
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();//计算当前集合个数有多少个
        }
        //扩容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //判断s集合总个数当前域值sizeCtl比较,while循环扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                //算出一个负数
                int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
                if (sc < 0) {
                    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //sc=12时候走到这里,cas操作,算出的rs,只有一个线程可以将sc改成一个负数,另一个线程就会走到上面
                else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
                //转移
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
//这个方法到底做什么的,对cell数组初始化数组长度2
//我们可以认为参数是  1,false
private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        //对线程生成一个随机数
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        //循环
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //不是null
            if ((as = counterCells) != null && (n = as.length) > 0) {
            	//如果当前线程对应的下标值为null
                if ((a = as[(n - 1) & h]) == null) {
                //cellsBusy == 0表示没有线程使用
                    if (cellsBusy == 0) {  
                    //new一个CounterCell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //cas改成0到1,占用。说明当前线程要用这个数组了,将new的对象放到这个数组中
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                //这里判断是否有其他线程对其修改,若没有将new的cell放到数组中
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                            //最后将标记归零
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                //再去生成一个新的hash,再次循环,再次生成新的下标
                    wasUncontended = true;      // Continue after rehash
                //cas对CELLVALUE进行+1
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            //counterCells是null的情况,初始化,
            //cellsBusy == 0 表示没有线程使用,cas操作0改成1表示有人用这个数组
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                    //new一个大小为2的数组
                        CounterCell[] rs = new CounterCell[2];
                        //x=1,属性赋值,value为1,这种场景初始化成功,把x也加上了
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //如果两个线程去初始化cell数组,然后一个线程失败了,走到这里,还是去cas改变baseCount的值
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

fwd 节点: 这个代表的是占位节点,最关键的就是这个节点的 hash 值为 -1,所以一旦发现某一个节点中的 hash 值为 -1 就可以知道当前节点已经被迁移了。
advance: 代表是否可以继续推进下一个槽位,只有当前槽位数据被迁移完成之后才可以设置为 true
finishing: 是否已经完成数据迁移。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //这里ForwordingNode对象,hash=-1
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        //一个线程转移有没有必要继续往前走去转移其他位置,
        boolean advance = true;
        //当前线程扩容工作是否完成,如果是true,代表当前线程做完了工作
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //第一次默认advance=true
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //第一次通过下面cas操作确认边界,通过下面算出当前线程要控制的区域。TRANSFERINDEX控制从哪个位置开始去计算(根据其去计算),其他线程cas失败后继续循环,从哪个位置开始计算,得到该线程的区域
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                //全部转移完成后,finishing修改为true,这里全部转移完成才会属性赋值table,转移结束。
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                //判断线程数,看整个集合是否全部完成,如果不等于,说明还有线程没有完成,这里判断sc是否是初始值,如果等于初始值,说明所有线程都结束了扩容,将advance和finish改成true。
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                //将元素转移到新的上去,
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                        //这里和hashmap扩容转移一样的
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

ConcurrentHashMap 的扩容

接下来我们回到 addCount 方法,这个方法在添加元素数量的同时,也会判断当前 ConcurrentHashMap 的大小是否达到了扩容的阈值,如果达到,需要扩容。
在多并发下如何实现扩容才不会冲突呢?可能大家都想到了采用分而治之的思想,在 ConcurrentHashMap 中采用的是分段扩容法,即每个线程负责一段,默认最小是 16,也就是说如果 ConcurrentHashMap 中只有 16 个槽位,那么就只会有一个线程参与扩容。如果大于 16 则根据当前 CPU 数来进行分配,最大参与扩容线程数不会超过 CPU 数。
初始化好了新的数组,接下来就是要准备确认边界。也就是要确认当前线程负责的槽位,确认好之后会从大到小开始往前推进,比如线程一负责 1-16,那么对应的数组边界就是 0-15,然后会从最后一位 15 开始迁移数据,每个线程操作自己的区域。
迁移完成后,会进行判断线程是否都执行完成,然后才进行tab属性赋值操作。
while 循环彻底结束之后,会进入到下面这个 if 判断,红框中就是当前线程自己完成了迁移之后,会将扩容线程数进行递减,递减之后会再次通过一个条件判断,这个条件其实就是前面进入扩容前条件的反推,如果成立说明扩容已经完成,扩容完成之后会将 nextTable 设置为 null,所以上面不满足扩容的第 4 个条件就是在这里设置的。

重点:ConcurrentHashMap的计数和扩容
这里都是多线程,计数是通过一个cell数组去实现,扩容是每个线程都有自己的区域,然后进行迁移,将数据放到新数组上面去。都是分而治之的思想。

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

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

相关文章

有人说ChatGPT信息不新?

Hello ,我是小索奇&#xff0c;今天给大家分享一个插件&#xff0c;这个插件可以通过抓取网页获得最新内容&#xff0c;它可以有效的避免ChatGPT信息过时&#xff0c;获取不到最新的信息等等 演示-这里问它一些问题&#xff1a; 现在几点了呀 可以看到时间也是很准确的&#x…

Linux权限(+Linux基本指令(下))

目录 一.基本指令补充 1.date指令 2.find指令 3.tar指令 4.Linux下的常用热键 二.Linux权限 1.Shell 2.Linux权限的概念 一.基本指令补充 1.date指令 &#x1f606;date指令可以用于显示日期和时间戳&#x1f606;Linux的时间戳与Unix时间戳一致,指的是从1970年1月1日…

使用无标注的数据训练Bert

文章目录 1、准备用于训练的数据集2、处理数据集3、克隆代码4、运行代码5、将ckpt模型转为bin模型使其可在pytorch中运用 Bert官方仓库&#xff1a;https://github.com/google-research/bert 1、准备用于训练的数据集 此处准备的是BBC news的数据集&#xff0c;下载链接&…

Python | 人脸识别系统 — UI界面设计

本博客为人脸识别系统的UI界面设计代码解释 人脸识别系统博客汇总&#xff1a;人脸识别系统-博客索引 项目GitHub地址&#xff1a;【待】 注意&#xff1a;阅读本博客前请先参考以下博客 工具安装、环境配置&#xff1a;人脸识别系统-简介 阅读完本博客后可以继续阅读&#xff…

不用下载就能使用的4款轻量在线PS工具

PS是一种非常熟悉的设计工具&#xff0c;也是一种在设计领域占有重要地位的软件&#xff0c;如常见的产品设计、平面设计或摄影后期设计&#xff0c;几乎与PS的使用密不可分。PS本身也有很多功能&#xff0c;每个人的日常设计图纸、图纸修复等工作都可以用PS完成。 但PS有很多…

yolov8 OpenCV DNN 部署 推理报错

yolov8是yolov5作者发布的新作品 目录 1、下载源码 2、下载权重 3、配置环境 4、导出onnx格式 5、OpenCV DNN 推理 1、下载源码 git clone https://github.com/ultralytics/ultralytics.git 2、下载权重 git clone https://github.com/ultralytics/assets/releases/dow…

MySQL知识学习05(InnoDB存储引擎对MVCC的实现)

1、一致性非锁定读和锁定读 一致性非锁定读 对于 一致性非锁定读&#xff08;Consistent Nonlocking Reads&#xff09; &#xff0c;通常做法是加一个版本号或者时间戳字段&#xff0c;在更新数据的同时版本号 1 或者更新时间戳。查询时&#xff0c;将当前可见的版本号与对…

K8S资源-configmap创建六种方式

云原生实现配置分离重要实现方式 两者都是用来存储配置文件&#xff0c;configmap存储通用的配置文件&#xff0c;secret存储需要加密的配置文件。 将配置文件configmap挂在到pod上 创建configmap 1.基于配置文件目录创建configmap kubectl create cm cmdir --from-fileconf…

医学图像分割之U-Net

一、背景及问题 在过去两年中&#xff0c;在很多视觉识别任务重&#xff0c;深度卷积网络的表现优于当时最先进的方法。但这些深度卷积网络的发展受限于网络模型的大小以及训练数据集的规模。虽然这个限制有过突破&#xff0c;也是在更深的网络、更大的数据集中产生的更好的性能…

【redis】redis的缓存过期淘汰策略

【redis】redis的缓存过期淘汰策略 文章目录 【redis】redis的缓存过期淘汰策略前言一、面试题二、redis内存满了怎么办&#xff1f;1、redis默认内存是多少&#xff1f;在哪查看&#xff1f;如何修改?在conf配置文件中可以查看 修改&#xff0c;内存默认是0redis的默认内存有…

使用意图intent构建一个多活动的Android应用

安卓意图Intent是Android应用组件(Activity、Service、Broadcast Receiver)之间进行交互的一种重要方式。Intent允许启动一个活动、启动一个服务、传递广播等。Intent使应用能够响应系统及其他应用的动作。Intent使用的主要目的有: 1、 启动Activity:可以启动自己应用内的Activ…

DDPM--生成扩散模型

DDPM–生成扩散模型 Github: https://github.com/daiyizheng/Deep-Learning-Ai/blob/master/AIGC/Diffusion.ipynb DDPM 是当前扩散模型的起点。在本文中&#xff0c;作者建议使用马尔可夫链模型&#xff0c;逐步向图像添加噪声。 函数 q ( x t ∣ x t − 1 ) q(x_t | x_t-1…

java获取真实ip的方法

在网络中&#xff0c;如果不想被人监听&#xff0c;那么就需要获取 IP地址了&#xff0c;在电脑中我们可以使用到 ip地址获取工具&#xff0c;那么如何在 Java中获取真实的 IP地址呢&#xff1f; 1、首先我们需要先准备一台电脑&#xff0c;然后将电脑进行联网&#xff1b; 2、…

ChatGPT带你一起了解C语言中的fseek()

fseek函数用于将文件指针移动到指定位置。它的原型如下&#xff1a; c int fseek(FILE *stream, long offset, int whence); 其中&#xff0c;stream是文件指针&#xff0c;offset是偏移量&#xff0c;whence是起始位置。 偏移量offset可以是正数、负数或零。 如果是正数&a…

Java --- springboot2数据响应与内容协商

目录 一、数据响应与内容协商 1.1、响应json 1.1.1、返回值解析器 1.1.2、springMVC支持的返回值类型 1.1.3、HttpMessageConverter原理 1.2、内容协商 1.2.1、引入依赖 1.2.2、 postman分别测试返回json和xml 1.2.3、开启浏览器参数方式内容协商功能 1.3、自定义 Message…

持续测试:DevOps时代质量保证的关键

在 DevOps 时代&#xff0c;持续测试已成为质量保证的一个重要方面。近年来&#xff0c;软件开发方法论发生了快速转变。随着 DevOps 的出现&#xff0c;已经发生了向自动化和持续集成与交付 (CI/CD) 的重大转变。传统的质量保证方法已不足以满足现代软件开发实践的需求。持续测…

Java——二叉树的深度

题目链接 牛客网在线oj题——二叉树的深度 题目描述 输入一棵二叉树&#xff0c;求该树的深度。从根结点到叶结点依次经过的结点&#xff08;含根、叶结点&#xff09;形成树的一条路径&#xff0c;最长路径的长度为树的深度&#xff0c;根节点的深度视为 1 。 数据范围&am…

记一次产线打印json导致的redis连接超时

服务在中午十一点上线后&#xff0c;服务每分钟发出三到四次redis连接超时告警。错误信息为&#xff1a; Dial err:dial tcp: lookup xxxxx: i/o timeout 排查过程 先是检查redis机器的情况&#xff0c;redis写入并发数较大&#xff0c;缓存中保留了一小时大概400w条数据。red…

java学习之第十章作业

目录 第一题 第二题 第三题 第四题 第五题 第六题 代码的问题点 第七题 第八题 第一题 package homework;public class HomeWork01 {public static void main(String[] args) {Car c new Car();//创建新对象&#xff0c;没有实参Car c1 new Car(100);//1.创建一个新的…

Windows11开启远程桌面和修改远程端口

该示例适用于大部分的Windows平台&#xff0c;示例基于Windows 11。操作系统&#xff1a;Windows 11 专业版。远程桌面默认使用TCP协议&#xff0c;默认端口为3389&#xff0c;修改后为13389。 一、开启远程桌面 控制面板-->系统与安全-->系统-->允许远程访问 二、修…