【并发编程】ConcurrentHashMap源码分析(一)

news2024/11/13 4:00:57

ConcurrentHashMap源码分析

  • CHM的使用
  • CHM的存储结构和实现
  • CHM源码
    • put源码分析
      • initTable 初始化table
      • treeifyBin()和tryPresize()
      • transfer 扩容和数据迁移
        • 高低位的迁移

ConcurrentHashMap是一个高性能的,线程安全的HashMap

HashTable线程安全,直接在get,put方法上加了synchronized关键字
1.7 CHM使用的 segment分段锁,锁的粒度较大

需要在性能-安全性之间做好平衡

CHM的使用

基本使用同HashMap,

  • put()
  • get()
    jdk1.8还支持使用lambda表达式
  • computeIfAbsent : key如果不存在,调用后面的mappingFunction方法
  • computeIfPresent : key如果存在,调用后面的mappingFunction方法更新值
  • compute (computeIfPresent 和computeIfAbsent 的结合)
  • merge :合并数据

CHM的存储结构和实现

1.8去掉了segment,并且引入了红黑树(数组长度大于64,链表长度大于8,则会将链表转为红黑树),引入红黑树是为了提高检索性能,时间复杂度 O(n)->O(logn)在这里插入图片描述

CHM源码

put源码分析

   final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //计算Hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        //自旋(;;) CAS
        for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
            ConcurrentHashMap.Node<K,V> f; int n, i, fh;
            //如果tab为空,则初始化table(需要加锁保证线程安全CAS)
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();//初始化完成后进入下一次循环
            //(n - 1) & hash 计算数组下标, 从内存的偏移量获取值
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //如果当前node的位置为null,则直接存储到该位置,通过CAS保证原子性
                if (casTabAt(tab, i, null,
                        new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前节点状态为MOVED,说明当前该位置处于迁移中,则该线程先帮忙扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //锁住当前的node节点 synchronized 保证线程安全
                synchronized (f) {
                    //重新判断
                    if (tabAt(tab, i) == f) {
                        //针对链表处理, binCount统计当前节点的链表长度
                        if (fh >= 0) {
                            binCount = 1;
                            for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //判断是否存在相同的key,存在则覆盖
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                ConcurrentHashMap.Node<K,V> pred = e;
                                //如果不存在则将key/value添加到链表中
                                if ((e = e.next) == null) {
                                    pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //针对红黑树
                        else if (f instanceof ConcurrentHashMap.TreeBin) {
                            ConcurrentHashMap.Node<K,V> p;
                            binCount = 2;
                            if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //如果链表长度>=8,会调用treeifyBin方法
                    if (binCount >= TREEIFY_THRESHOLD)
                        //根据阈值和数组大小判断是扩容还是转换为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //size++的实现,增加计数
        addCount(1L, binCount);
        return null;
    }

initTable 初始化table

 private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //自旋,只要table没有初始化成功则不断自旋直到完成初始化
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //通过CAS自旋来抢占一个锁标记(sizeCtl),将sizeCtl设置为-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                //说明当前线程抢占到了锁
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //sc设置为扩容的阈值(0.75*n)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //修改sizeCtl,释放锁
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

sizeCtl不同值表示不同的状态:
在这里插入图片描述

treeifyBin()和tryPresize()

    private final void treeifyBin(ConcurrentHashMap.Node<K,V>[] tab, int index) {
        ConcurrentHashMap.Node<K,V> b; int n, sc;
        if (tab != null) {
            //如果tab长度小于64,调用tryPresize扩容
            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) {
                        ConcurrentHashMap.TreeNode<K,V> hd = null, tl = null;
                        for (ConcurrentHashMap.Node<K,V> e = b; e != null; e = e.next) {
                            ConcurrentHashMap.TreeNode<K,V> p =
                                    new ConcurrentHashMap.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 ConcurrentHashMap.TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

  • 创建一个新的数组
  • 对老的数组的数据进行迁移
  • 多线程辅助扩容(针对老的数据,通过多个线程并行来执行数据的迁移过程)
    • 记录当前的线程数量(sizeCtl)
    • 当每个线程完成数据迁移之后,退出的时候,要减掉协助扩容的线程数量
  • resizeStamp -> 扩容戳
 //扩容方法
    private final void tryPresize(int size) {
        //确定扩容后的目标大小  tableSizeFor会将size转为2的n次方-1的值
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //说明不处于初始化中或者扩容中
        while ((sc = sizeCtl) >= 0) {
            ConcurrentHashMap.Node<K,V>[] tab = table; int n;
            //tab==null则初始化tab
            if (tab == null || (n = tab.length) == 0) {
                //实际初始化的大小从sc和c中选择较大的那个
                n = (sc > c) ? sc : c;
                //CAS抢占锁标记,将sizeCtl设置为-1
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //已经是最大容量了,无法再继续扩容,直接返回
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                //多线程参与扩容,需要有一个地方记录有多少个线程参与了扩容(数据迁移),这样才可以判断是否所有线程都已经完成迁移工作
                int rs = resizeStamp(n);//rs为扩容戳,记录有多少个线程参与扩容,保证当前扩容范围的唯一性
                //sc<0表示扩容中,第一次进入的时候sc>=0
                if (sc < 0) {
                    ConcurrentHashMap.Node<K,V>[] nt;
                    //表示扩容结束
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    //表示扩容没有结束,每次进入这个方法则表示有一个新的线程执行transfer方法进行扩容
                    //每增加一个扩容线程,则在低位+1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //第一次进入的时候,会调用transfer方法进行扩容 CAS成功后,sc会变为负数
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

transfer 扩容和数据迁移

  • 数据迁移
    • 需要计算当前线程的数据迁移的空间(拆分迁移任务给多个线程)
    • 创建一个新的数组(n=old length*2)
    • 实现数据迁移(高低位迁移)
      • 如果是红黑树
        数据迁移后,不满足红黑树的条件,则转为链表
      • 如果是链表

在这里插入图片描述

 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // NCPU 表示当前机器的 CPU 核心数,计算每个线程处理的数据的区间大小stride,最小是16
        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 = old table[] 的长度。
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //用来表示已经迁移完的状态,也就是说,如果某个old数组的节点完成了迁移,则需要更改成fwd。(fwd的hash为-1,就是MOVED值)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        //死循环,完成扩容后代码里会有退出循环的条件 finishing==true则结束循环
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //计算要迁移的数组区间
            while (advance) {
                int nextIndex, nextBound;
                //--i>= bound,说明当前数据还是在本次的处理范围内,那么不需要重新计算区间
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //stride为每个线程要处理的区间大小,transferIndex初始值为oldTab的大小,这里通过CAS
                //更新transferIndex的值
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    bound = nextBound;
                    //计算需要处理的区间后,当前线程要处理的数据范围就是[nextBound,i)
                    i = nextIndex - 1;
                    advance = false;
                }
                //假设数组长度是32,
                //第一次 [16(nextBound),31(i)]
                //第二次 [0,15]
            }
            //判断是否扩容结束
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //对应于前面的+2
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //第一次i=31,则得到数组下标为31的位置的值。(这里就是old tab下标最大的那个位置的值)
            else if ((f = tabAt(tab, i)) == null) //说明当前数组位置为空,不需要迁移。
                advance = casTabAt(tab, i, null, fwd); //直接改成fwd -> 表示迁移完成
            // 判断是否已经被处理过了,如果是则进入下一次区间遍历(MOVED是fwd节点的初始状态)
            // 下一次遍历因为--i>= bound所以又会走到这里的逻辑
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                //加锁->针对当前要去迁移的节点,保证迁移过程中,其他线程调用put()向这个位置插入数据时,必须要等待。
                synchronized (f) { //
                    //针对不同类型的节点,做不同的处理 链表 or 红黑树
                    //链表迁移的时候,由于数组长度改变,那么同一个key计算出来的数组下标也不一样了,需要重新计算
                    if (tabAt(tab, i) == f) {
                        //两个链表,低位链表,高位链表, 低位链表示不需要更改数组下标的链表(1)高低链表单独讲解下
                        Node<K,V> ln, hn;
                        //fh表示当前节点的hashcode
                        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;
                                }
                            }
                            //runBit==0表示不需要更改数组下标
                            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) {
                            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;
                        }
                    }
                }
            }
        }
    }

高低位的迁移

先来讲一下为什么可以做高低位的迁移呢?

之前源码里数组下标的计算方式是: i = (n - 1) & hash
在扩容的时候n的值是变化的,会变为2*n,不同的key的hash值不同,所以扩容后,部分key可能会发生迁移(计算出来的下标不一样了)

注意,我们这里的n是2的整数次幂,那么n-1的二进制表示就会从1xx111 (原来有k个1),变为11xx111(k+1个1), 和hash进行与运算之后,其结果和之前的相比,可能会在第k+1位多了一个1,其他位和旧tab下的运算结果是一致的

转成十进制其实就是+n,是否下标会从i->n+i取决于key的hash的k+1位是1还是0(从低到高的k+1位)

int runBit = fh & n;就是用来判断原来的hash的第k+1位是否为1,如果runBit==0,则说明不是1(说明数据不会发生迁移),否则就是1

    //fh表示当前节点的hashcode,如果fh<0则表示该节点已经迁移完成了
    if (fh >= 0) {
        //runBit==0说明数据不会发生迁移
        int runBit = fh & n;
        Node<K,V> lastRun = f;
        //lastRun 机制选出最后hash值相同的链表的头节点(lastRun节点之后的节点的迁移后都在一个位置上)
        for (Node<K,V> p = f.next; p != null; p = p.next) {
            int b = p.hash & n;
            //该节点和前一个节点的runBit不一样,则会进入下面的代码
            if (b != runBit) {
                runBit = b;
                lastRun = p;
            }
        }
        //根据最高位Bit为1还是0,决定放入新数组的高位还是低位
        if (runBit == 0) {
            ln = lastRun;
            hn = null;
        }
        else {
            hn = lastRun;
            ln = null;
        }
        //迁移剩余的节点,ph & n == 0存入低位,1则存入高位
        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);
        }
        //将低位链表放在i的位置上
        setTabAt(nextTab, i, ln);
        //将高位链表放在i的位置上
        setTabAt(nextTab, i + n, hn);
        //更新fwd标识,表示已经迁移完数据
        setTabAt(tab, i, fwd);
        advance = true;
    }

关于size统计, 红黑树的介绍,get的源码分析在下一篇博客
ConcurrentHashMap源码分析(二)

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

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

相关文章

spring security 的AuthenticationSuccessHandler 没有调用 ,无法生效

今天想不明白&#xff0c;我控制层写了一个登录的接口。结果验证成功了&#xff0c;我发现AuthenticationSuccessHandler 没有调用 &#xff0c;而且也不生效啊&#xff0c;最后研究终于发现是因为我们需要配置登录的url 这个url 我们访问&#xff0c;中间的什么控制器什么的框…

Win10怎么取消开机密码?这样做就可以!

集美们&#xff0c;我每次开电脑都要输入密码&#xff0c;感觉太麻烦了&#xff0c;想把开机密码取消掉&#xff0c;应该怎么做呀&#xff1f;感谢回答&#xff01;】 在Windows 10操作系统中&#xff0c;用户可以设置开机密码来保护计算机的安全性。然而&#xff0c;有时候用…

【CSS】使用绝对定位 / 浮动解决外边距塌陷问题 ( 为父容器 / 子元素设置内边距 / 边框 | 为子元素设置浮动 | 为子元素设置绝对定位 )

文章目录一、外边距塌陷描述1、没有塌陷的情况2、外边距塌陷情况二、传统方法解决外边距塌陷 - 为父容器 / 子元素设置内边距 / 边框三、使用浮动解决外边距塌陷 - 为子元素设置浮动四、使用绝对定位解决外边距塌陷 - 为子元素设置绝对定位一、外边距塌陷描述 在 标准流的父盒子…

AE开发20210601之绘制集合要素、绘制点、空间查询、属性查询、图形查询、选择集内容、符号化

AE开发之绘制集合要素绘制点步骤queryFilterQueryDefcursor查现有的SpatialRel可以有的空间关系有哪一些。Map类&#xff0c;对应的是FeatureSelection属性&#xff0c;SelectionCount属性&#xff0c;空间查询属性查询图形查询选择集内容符号化multiparrcolorRampLineSymbol下…

a标签 链接 target=”_blank” 为什么要增加 rel=”noopener noreferrer”

在<a></a>中使用target"_blank" 那么会得到以下错误提示&#xff1a; Using target"_blank" without rel"noopener noreferrer" is a security risk: see https://mathiasbynens.github.io/rel-noopener [react/jsx-no-target-blank…

Golang流媒体实战之七:hls拉流服务源码阅读

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 本文是《Golang流媒体实战》系列的第七篇&#xff0c;继续学习一个重要且通用的知识点&#xff1a;hls拉流在《体验开源项目lal》一文中&#xff0…

两种方法实现杨辉三角(java实现)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

AI 作图绘画的软件和网址

软件分享 分享软件一&#xff1a;NovelAI NovelAI是一个用于ai写文章的工具&#xff0c;大家可以给它文章标题框架&#xff0c;让它生成文章。后来这款工具进行功能拓展&#xff0c;完成了ai绘画功能&#xff0c;它支持我们上传草图生成图片&#xff0c;也支持我们通过画面描…

计算机网络思维导图,快快收藏学习啦!

第一章&#xff08;概述&#xff09; P0 - 计算机网络<思维导图> 第二章&#xff08;物理层&#xff09; P1 - 计算机网络<思维导图> 便签中的内容&#xff1a; ①香农公式&#xff1a;CW*Log2(1S/N) (bit/s) C:极限传输速率 W:信道带宽(单位Hz) S:信道内所传…

graylog实现日志监控

graylog graylog是一个轻量级的日志管理工具,依托elasticsearch作为日志存储中间件,MongoDB作为元数据信息存储中间件.自带WEB-UI界面,LDAP整合各种日志类型.提供了日志收集、日志查询、监控告警等相关功能。提供了graylog sidecar通过sidecar模式可以很方便的收集目标主机、容…

UEFI Protocol

一、概述 二、Protocol的定义 1、Protocol是服务器端和客户端之间的一种约定&#xff0c;在软件编程上称为接口&#xff0c;服务器端和客户端通过这个约定信息的互通。 2、服务器端和客户端在UEFI中都是可执行的二进制文件。 3、为了实现这些二进制文件之间的互通&#xff0c;…

nginx反向代理_负载均衡的配置

说明 两台虚拟机&#xff1a; 88节点是自己的虚拟机 66节点是小组成员的虚拟机&#xff0c;我们暂且叫同学机 tomcat端口&#xff0c;分别为8081和8082 总结就是&#xff1a; 自己虚拟机上面安装nginx和tomcat8082 同学机上安装tomcat8081 一、开始安装nginx&#xff08;只安装…

香港布局Web3.0 既是金融试探,也是未来战略

香港Web3.0协会成立的消息已在业内刷屏&#xff0c;作为跨业界的非盈利机构&#xff0c;该协会致力于促进Web3.0生态环境的建设&#xff0c;港府特首李家超和北京中央驻港联络办公室部分领导均出席了成立典礼。 李家超在致辞中表示&#xff0c;Web3.0的发展正值黄金起点&#x…

Vue随记

1、Vue模板语法 Vue模板语法有两大类&#xff1a; 1.1、插值语法 功能&#xff1a;用于解析标签体内容。 写法&#xff1a;{{xxxx}}&#xff0c;xxxx是js表达式&#xff0c;且可以直接读取到data中的所有属性。 1.2、指令语法 功能&#xff1a;用于解析标签&#xff08;包…

《鸟哥的Linux私房菜-基础篇》学习笔记

主要用来记录学习&#xff0c;如果能帮助到你那最好了。 数据流重导向 概念 cat /etc/crontab /etc/vbirdsay 标准输出&#xff1a;将cat的文件输出到屏幕上 标准错误输出&#xff1a;无法找到文件报错 *系统会将标准输出和标注错误输出都输出到屏幕上&#xff0c;看着比较乱…

Vue双向数据绑定原理

一. Vue双向数据绑定原理 Vue.js的双向绑定是通过响应式原理实现的。响应式原理就是当数据发生改变时&#xff0c;自动更新相关的视图和数据。下面是Vue.js双向绑定的详细解释和介绍&#xff1a; Vue.js通过 Object.defineProperty() 将数据对象的属性值绑定到对应的DOM元素上…

【数据结构启航!】数据结构开胃菜之顺序表

【数据结构启航&#xff01;】数据结构开胃菜之顺序表一、线性表简介二、目标三、实现1、初始化工作2、顺序表的尾插2.1、图解原理2.2、代码实现3、顺序表的尾删3.1、图解原理3.2、代码实现4、打印顺序表5、顺序表的增容6、顺序表的头插6.1、图解原理6.2、代码实现7、顺序表的头…

Javascript cookie和session

在网站中&#xff0c;http请求是无状态的&#xff0c;当我们与服务端做一次数据请求&#xff0c;请求完毕后&#xff0c;第二次数据请求服务器端仍然不知道是哪个用户&#xff0c;cookie的出现就是为了解决这个问题。 一 Session与Cookie的区别 1 相同点 它们都是用于存…

实战打靶集锦-016-lampiao

提示&#xff1a;本文记录了博主打靶过程中一次曲折的提权经历 文章1. 主机发现2. 端口扫描3. 服务枚举4. 服务探查4.1 80端口探查4.2 1898端口探查4.3 EXP搜索4.3.1 exploit/unix/webapp/drupal_coder_exec4.3.2 exploit/unix/webapp/drupal_drupalgeddon25. 提权5.1 系统信息…

RayVentory crack,RayVentory扫描引擎

RayVentory crack,RayVentory扫描引擎 RayVentory扫描引擎12.5.3581.73[更新2] libcurl.dll库现在使用Raynet证书进行了签名&#xff0c;为用户增加了额外的安全层。 对设备更新过程进行了显著改进&#xff0c;特别是在同时扫描具有大型数据库的多个设备时。这确保了数据的一致…