集合类源码浅析のJDK1.8ConcurrentHashMap(下篇)

文章目录

  • 前言
  • 一、分段扩容
    • 1、addCount
    • 2、transfer
    • 3、helpTransfer
  • 二、查询
  • 二、删除
  • 总结


前言

  主要记录ConcurrentHashMap(笔记中简称CHM)的查询,删除,以及扩容方法的关键源码分析。


一、分段扩容

1、addCount

  扩容的逻辑主要在addCount方法的后半段:

    private final void addCount(long x, int check) {
    		//....前半段是进行桶数组中所有元素的累加计数
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //满足条件就会一直循环:桶数组中所有元素的累加计数>=扩容阈值 并且 桶数组不为空 并且 桶数组的长度<最大长度
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                //通过对数组长度n进行计算得到的标记值
                int rs = resizeStamp(n);
                //sc为负数证明在哈希表正在扩容中(存在并发扩容的情况)
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //尝试将SIZECTL的值 + 1 代表有1个线程在协助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //否则通过CAS的操作尝试将SIZECTL设置成一个负数标记
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //真正扩容的逻辑                         
                    transfer(tab, null);
                //重新计算旧桶数组中所有元素的数量    
                s = sumCount();
            }
        }
    }

  其中resizeStamp(n),假设目前桶数组的length是16,那么计算出的值是:32795

        int rs = Integer.numberOfLeadingZeros(16) | (1 << (16 - 1));
        System.out.println("rs = " + rs);

  rs << RESIZE_STAMP_SHIFT) + 2计算出的是-2145714174。将SIZECTL设置为负数说明已经有一个线程去执行了扩容。

2、transfer

  transfer是真正执行扩容的逻辑:

	  //tab:旧表 nextTab:新表
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    		 //n 旧表的长度 stride 每个线程迁移的步长
        int n = tab.length, stride;
        //计算步长,最小为16 即每个线程最低负责处理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")
                //创建新表,长度为旧表的2倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                //将新表赋值给nextTab 变量(注意,此时还没有真正把新表指向CHM的nextTab 属性)
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //这一步才是将新表指向CHM的nextTable 属性
            nextTable = nextTab;
            //旧表的长度作为迁移的索引
            transferIndex = n;
        }
        //nextn 新表的长度
        int nextn = nextTab.length;
        //将新表包装成ForwardingNode对象。
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        //前进标记
        boolean advance = true;
        //结束标记
        boolean finishing = false; // to ensure sweep before committing nextTab

  到上面为止,只是初始化一些变量,以及在新表为空的情况下,创建出了新表,并赋值给了CHM的nextTable属性, 将旧表的长度赋值给CHM的transferIndex 属性,以及将新表包装成ForwardingNode对象,ForwardingNode的hash值是-1。

        //无限循环
        for (int i = 0, bound = 0;;) {
        		//f 是当前桶中的节点,fh 是节点的哈希值
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                //先将属性transferIndex赋值局部变量nextIndex。给如果 transferIndex 小于等于 0,表示没有任务可做,退出迁移过程。
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //通过CAS保证不会两个线程竞争到同一个区域
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }

  对于上面这一段的逻辑,可以画图表示,假设旧表的长度为4,每次迁移的步长为2:
在这里插入图片描述   在else if ((nextIndex = transferIndex) <= 0) 这一步,将nextIndex 赋值为了4:
在这里插入图片描述  在nextBound = (nextIndex > stride ? nextIndex - stride : 0))这一步,计算出nextBound = 4 - 2 = 2,并且将TRANSFERINDEX通过CAS改成了2
在这里插入图片描述
  最后经过bound = nextBound;i = nextIndex - 1; 计算出:
在这里插入图片描述  然后将advance = false,跳出while循环。
  上面的过程,假设又有另一个线程进入了while循环,在else if ((nextIndex = transferIndex) <= 0) 这一步,将nextIndex 赋值为了2(第一个线程通过CAS修改的):
在这里插入图片描述
  然后在nextBound = (nextIndex > stride ? nextIndex - stride : 0))这一步,计算出nextBound = 0,并且将TRANSFERINDEX通过CAS改成了0:
在这里插入图片描述  最后经过bound = nextBound;i = nextIndex - 1; 计算出:
在这里插入图片描述  也就是线程一负责2,3桶的迁移,线程二负责0,1桶的迁移。当然在不存在多线程并发的情况下,某个线程在迁移完了2,3桶后,依旧会判断是否要前进进行0,1桶的迁移。

					//i相比较于数组发生越界,当前线程已经将自己的区域转移完毕
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //结束标记为true时,这里不能仅仅根据当前线程判断结束标记,需要所有参与迁移的线程全部结束
                if (finishing) {
                		 //将CHM的临时新表指向null
                    nextTable = null;
                    //将CHM的table属性指向新表
                    table = nextTab;
                    //将CHM的sizeCtl 属性设置为 扩容阈值(计算是按照0.75的扩容因子进行的)
                    //n:32时 经过计算是 24
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //尝试CAS将SIZECTL - 1 标记当前线程已经完成了迁移。
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                		 //仍然有其他线程未释放标记
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //所有线程都迁移完成
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }

  当某个线程在CAS成功,第一次进入transfer时,SIZECTL会被更改成一个负数,其他的线程需要帮助扩容,则会对SIZECTL + 1:
在这里插入图片描述  SIZECTL(sc)的初始值是 (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2:
在这里插入图片描述  而上面的代码片段中,if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) 可以转换为 sc != resizeStamp(n) << RESIZE_STAMP_SHIFT) +2,代表依旧存在其他线程没有完成迁移,否则sc会复位成最初的 (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2

					//某个桶的头节点为空
            else if ((f = tabAt(tab, i)) == null)
                //就尝试通过CAS将null值替换为fwd节点
                advance = casTabAt(tab, i, null, fwd);
            //某个桶的头节点的hash值为-1 证明已经转换成了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) {
                            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;
                        }
                    }
                }
            }
        }
    }

  上面的代码片段,主要是迁移元素的逻辑,也是分为链表和红黑树,并且某个桶在迁移完成后,会标记为为fwd节点。其他线程在put时,发现当前桶的下标为fwd节点,则会走帮助扩容的逻辑:
在这里插入图片描述  链表的迁移,主要做了以下四步,和HashMap根据高低位进行分组是同样的思路:

  1. 遍历链表中的节点,找出链表中哈希值相同的分组。
  2. 根据分组将节点分别插入到两个新的链表中。
  3. 将这两个新链表插入到新的哈希表的相应位置。
  4. 将原来的链表标记为已处理。

  int runBit = fh & n; 是用当前节点的hash值去 & 旧表的长度,结果只有可能是0或者旧表的长度,例如:

        System.out.println(2365651 & 4);
        System.out.println(1926817 & 4);
        System.out.println(1848602 & 4);
        System.out.println(4731302 & 4);

在这里插入图片描述

3、helpTransfer

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        //会再次判断传入的tab 是否为空 以及桶下标位置的头节点是否为fwd 以及 fwd对象中的nextTable 是否为空
        //防止在帮助扩容的过程中,新表已经迁移完成并且赋值给了CHM的table属性
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            //根据旧表的长度计算得到一个标记
            int rs = resizeStamp(tab.length);
            //sizeCtl < 0 代表正在有其他线程在扩容
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                 //扩容已经结束  
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                 //尝试将SIZECTL + 1 标记当前线程正在参与扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            //返回临时的新表
            return nextTab;
        }
        //返回新表
        return table;
    }

二、查询

  这一段查询的逻辑,和HashMap类似,区别在于加入了对于fwd节点的判断,如果当前的节点是fwd节点,就去临时的新表中查询。

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        //再次判断桶数组是否不为空,以及根据hash算出的桶的头节点是否不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //查找的节点是根据hash算出的桶的头节
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                		//直接返回值
                    return e.val;
            }
            //hash值小于0 是fwd节点 或者红黑树
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //处理链表的查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        //没有找到就返回空值
        return null;
    }

二、删除

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

  删除的逻辑,也加入了判断,如果当前要删除的桶的头节点是MOVED,则和新增元素一样,去走帮助扩容的逻辑,并且删除也是对单个桶去加锁的,其他的逻辑和HashMap大致相似,也是分为链表和红黑树,先遍历找出需要删除的元素,再执行删除逻辑:

    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

总结

  在CHM的新增/删除时,如果发现目标节点的状态是MOVED,就说明旧表数据正在被迁移,于是当前线程会走帮助扩容的逻辑。CHM的扩容迁移,也是分段进行的,每个线程会计算步长(最小为16),也就是至少负责对16个桶元素的迁移。
  如果是单线程的情况下,当前线程在处理完16个桶后,会继续处理后续的16个桶。如果是多线程的场景下,通过CAS保证不会有多个线程同时处理相同步长范围内的桶。并且链表/红黑树迁移的过程,也是和HashMap的类似,通过节点的hash值 & 旧表长度计算出高位低位进行分组迁移,当某个桶下标迁移完成后,会将当前位置标记为fwd节点。 最后需要等到所有的线程都迁移完成后,才会真正将新表的引用指向CHM的table属性。

下一篇:集合类源码浅析のJDK1.8 HashMap中的红黑树

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

H5页面多个视频如何只同时播放一个?

目录 背景1. 首先介绍下 muted 属性2. 监听播放和暂停操作3. 视频播放完毕后返回桌面&#xff0c;再进入H5页面发现视频封面丢失置灰解决思路&#xff1a; 背景 页面模块同时有个四个视频模块&#xff0c;发现可以同时播放四个视频&#xff0c;但是理想的是每次只播放一个。 …

D69【 python 接口自动化学习】- python 基础之数据库

day69 Python 执行 SQL 语句 学习日期&#xff1a;20241115 学习目标&#xff1a; MySQL 数据库&#xfe63;- Python连接redis 学习笔记&#xff1a; redis数据库的用途 使用Python访问redis数据库 使用Python对redis数据库进行读写操作 总结 1. redis是一款高性能的键…

jmeter常用配置元件介绍总结之逻辑控制器

系列文章目录 安装jmeter jmeter常用配置元件介绍总结之逻辑控制器 逻辑控制器1.IF控制器2.事务控制器3.循环控制器4.While控制器5.ForEach控制器6.Include控制器7.Runtime控制器8.临界部分控制器9.交替控制器10.仅一次控制器11.简单控制器12.随机控制器13.随机顺序控制器14.吞…

21.<基于Spring图书管理系统②(图书列表+删除图书+更改图书)(非强制登录版本完结)>

PS&#xff1a; 开闭原则 定义和背景‌ ‌开闭原则&#xff08;Open-Closed Principle, OCP&#xff09;‌&#xff0c;也称为开放封闭原则&#xff0c;是面向对象设计中的一个基本原则。该原则强调软件中的模块、类或函数应该对扩展开放&#xff0c;对修改封闭。这意味着一个软…

springboot实现简单的数据查询接口(无实体类)

目录 前言&#xff1a;springboot整体架构 1、ZjGxbMapper.xml 2、ZjGxbMapper.java 3、ZjGxbService.java 4、ZjGxbController.java 5、调用接口测试数据是否正确 6、打包放到服务器即可 前言&#xff1a;springboot整体架构 文件架构&#xff0c;主要编写框选的这几类…

【已解决】 Tomcat10.1.x使用JSTL标签库

IDEA创建Java EE项目&#xff0c;使用Spring Spring MVC MyBatis框架&#xff0c;使用maven管理依赖。项目当前的环境是&#xff1a; Tomat 10.1.28Maven 3.6.3JDK 17 项目的功能&#xff1a;读取数据库的report表中的数据&#xff0c;返回一个List集合对象reportList在JSP…

权限相关知识

1.Linux权限的概念 在说Linux权限的概念之前我来问大家一个问题&#xff0c;你们觉得什么是权限&#xff1f; 权限平时的体现呢&#xff0c;就比如不是校长的亲戚就不能逛办公室&#xff0c;没充会员的爱奇艺看不了VIP影视剧&#xff0c;没成会员的的蛋糕店拿不到会员价等等等…

uniapp如何i18n国际化

1、正常情况下项目在代码生成的时候就已经有i18n的相关依赖&#xff0c;如果没有可以自行使用如下命令下载&#xff1a; npm install vue-i18n --save 2、创建相关文件 en文件下&#xff1a; zh文件下&#xff1a; index文件下&#xff1a; 3、在main.js中注册&#xff1a…

[刷题]入门3.彩票摇奖

博客主页&#xff1a;算法歌者本篇专栏&#xff1a;[刷题]您的支持&#xff0c;是我的创作动力。 文章目录 1、题目2、基础3、思路4、结果 1、题目 链接&#xff1a;洛谷-P2550-彩票摇奖 2、基础 此题目考察数组、三重循环、自增操作的能力。 3、思路 写代码时候&#xf…

JVM垃圾回收详解(重点)

堆空间的基本结构 Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时&#xff0c;Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收 Java 堆是垃圾收集器管理的主要区域&#xff0c;因此也被称作 GC 堆&#xff08;Garbage Collected Heap&…

git rebase --continue解冲突操作

git rebase --continue解冲突操作 如果只是执行了 git rebase 命令&#xff0c;那么git会输出一下“错误”提示&#xff1a; There is no tracking information for the current branch. Please specify which branch you want to rebase against. See git-rebase(1) for detai…

腾讯地图GL JS点标识监听:无dragend事件的经纬度获取方案

引入腾讯地图SDK <!--腾讯地图 API--><script charset"utf-8" src"https://map.qq.com/api/gljs?librariestools&v1.exp&key***"></script>构建地图容器 <div class"layui-card"><div class"layui-car…

249: 凸包面积

解法&#xff1a; 使用Andrew算法【计算几何/凸包】安德鲁算法&#xff08;Andrews Algorithm&#xff09;详解_andrew算法求凸包-CSDN博客 排序&#xff1a; 将所有点按照x坐标进行升序排序。如果x坐标相同&#xff0c;则按照y坐标升序排序。 初始化栈&#xff1a; 使用一个栈…

基于VUE实现语音通话:边录边转发送语言消息、 播放pcm 音频

文章目录 引言I 音频协议音频格式:音频协议:II 实现协议创建ws对象初始化边录边转发送语言消息 setupPCM按下通话按钮时开始讲话,松开后停止讲话播放pcm 音频III 第三库recorderplayer调试引言 需求:电台通讯网(电台远程遥控软件-超短波)该系统通过网络、超短波终端等无线…

【Rust中的项目管理】

Rust中的项目管理 前言Package&#xff0c;Crate&#xff0c;Module &use &#xff0c;Path通过代码示例解释 Crate&#xff0c;Module &#xff0c;use&#xff0c;Path创建一个package&#xff1a;代码组织化skin.rs 中的代码struct & enum 相对路径和绝对路径引用同…

极客争锋 智连未来 TuyaOpen Framework极客创意大赛正式开启

TuyaOpen Framework极客创意大赛正式开启 可选择基于: TuyaOpen Framework 原生开源包: https://github.com/tuya/tuyaopen 支持 Ubuntu/T2/T3/T5/ESP32/ESP32C3等多款芯片TuyaOpen Arduino:https://github.com/tuya/arduino-tuyaopen支持 T2/T3/T5等多款芯片TuyaOpen LuaNode…

安装SQL server中python和R

这两个都是编程语言 R 是一种专门为统计计算和数据分析而设计的语言&#xff0c;它具有丰富的统计函数和绘图工具&#xff0c;常用于学术研究、数据分析和统计建模等领域。 Python 是一种通用型编程语言&#xff0c;具有简单易学、语法简洁、功能强大等特点。它在数据科学、机…

A029-基于Spring Boot的物流管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

理解HTTP中的Cookie与Session:机制、安全性与报头响应

文章目录 1. HTTP Cookie1.1. HTTP Cookie 工作流程1.2. Cookie 分类1.3. 安全性主要用途 2. Set-Cookie 报头2.1. Set-Cookie 格式2.2. 生命周期 3. HTTP Session3.1. 工作流程3.2. 安全性3.3. 超时 与 失效3.4. 用途 1. HTTP Cookie HTTP Cookie&#xff08;也称为 Web Cook…

【电脑】解决DiskGenius调整分区大小时报错“文件使用的簇被标记为空闲或与其它文件有交叉”

【电脑】解决DiskGenius调整分区大小时报错“文件使用的簇被标记为空闲或与其它文件有交叉” 零、报错 在使用DiskGenius对磁盘分区进行调整时&#xff0c;DiskGenius检查出磁盘报错&#xff0c;报错信息&#xff1a;文件使用的簇被标记为空闲或与其它文件有交叉&#xff0c;…