完蛋!我被ConcurrentHashMap源码包围了!(一)

news2024/11/15 20:49:30

文章目录

  • 1. 引言
  • 2. 使用
  • 3. 初始化
  • 4. 存储流程
  • 5. 取值流程
  • 6. 扩容流程

1. 引言

ConcurrentHashMap是一个线程安全的HashMap,在JDK1.7与JDK1.8,无论是实现还是数据结构都会有所不一样。这促使了ConcurrentHashMap有着HashMap一样的面试高频考点。

接下来,我将会以下面几点带硬核大家从源码角度理解ConcurrentHashMap的整体流程,开始发车!

image-20231125103018463

注意:若文章无特殊说明均代表JDK1.8的ConcurrentHashMap


2. 使用

在进入源码学习之前,先回忆一下ConcurrentHashMap是如何使用的。

public static void main(String[] args) {
    Map<String, String> map = new ConcurrentHashMap<>();
    map.put("a", "b");
    map.put("b", "c");
    map.put("c", "d");
    System.out.println(map.get("a"));
}

ConcurrentHashMap简单使用如上,不过多赘述。


3. 初始化

想学学习一个类的源码,就必须由浅入深,先从构造方法开始学习。

无参构成,没啥好聊的

public ConcurrentHashMap() {
}

有参构造,构造参数为初始化容量

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        // initialCapacity为0 抛异常
        throw new IllegalArgumentException();
    // 判断初始化容量参数initialCapacity 与 MAXIMUM_CAPACITY >>> 1 的大小
    // 如果 initialCapacity 大于等于 MAXIMUM_CAPACITY >>> 1
    // 则取 MAXIMUM_CAPACITY 为容量
    // MAXIMUM_CAPACITY 是Map的最大容量
    // 如果 initialCapacity 小于 MAXIMUM_CAPACITY >>> 1
    // 找出距离initialCapacity最近的2次幂
    // 为什么要2次幂????别急 后面会聊到。
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

// 根据传递进来的参数,找出这个参数最近的2次幂
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

有参构造,构造参数为一个Map

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 容量大小初始化为默认的容量16
    this.sizeCtl = DEFAULT_CAPACITY;
    // 将Map的元素全部put进去
    putAll(m);
}

4. 存储流程

使用ConcurrentHashMap将一个键值对放进Map的时候,我们通常调用put方法

public V put(K key, V value) {
    // 在put方法中,并没有做太多的事情,而是直接调用了putVal方法
    // 对于putVal方法,有三个参数,key-value就没啥好说的,就是需要存储的key-value值
    // 第三个参数传递一个boolean
    // 如果为false,代表如果Key存在了,直接覆盖数据
    // 如果为true,代表如果Key存在了,什么都不做
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许Key 或 Value 当中有一个为null
    // 为啥呢?
    // 是因为ConcurrentHashMap的应用场景是多线程场景下,如果Key或Value为null容易出现歧义
    // 毕竟无法得知Key 或 Value为null,是因为本身存储的就是null还是因为其他线程修改导致出现的null
    if (key == null || value == null) throw new NullPointerException();
    // 计算哈希值,请看下面的spread方法
    int hash = spread(key.hashCode());
    int binCount = 0;
    // tab指向table, table就是JDK1.8中ConcurrentHashMap的Node数组
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果tab为null 或 table长度为0 那么进行初始化table操作
        if (tab == null || (n = tab.length) == 0)
            // 请看下面的initTable方法解释
            tab = initTable();
        // tabAt方法的详解请看下面
        // (n - 1) & hash 是计算hash对应的索引下标,判断table对应的这个索引下标是否有值
        // 通过CAS获取table对应索引下标的值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果table数组在i索引下标位置没有值,利用CAS插入
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 能走到这个else-if 说明hash值计算出来的索引下标在table中存在值了
        // f是一个Node数组的一个元素
        // 取出这个Node元素的hash值,如果哈希值为MOVED,那么代表当前hash位置的数据正在扩容
        // static final int MOVED     = -1
        else if ((fh = f.hash) == MOVED)
          	// 扩容机制后面再聊
            tab = helpTransfer(tab, f);
        else {
            // 能走到这里,说明hash值计算出来的索引下标在table中存在值了,并且当前不处于扩容
            // 就需要往链表里面插入数据了 往链表插入数据,需要锁当前Node数组下标i的数据块
            V oldVal = null;
            synchronized (f) {
                // 校验一下table在i的下标的下标是不是等于f
                // 这是一个双重校验,校验一下索引下标i的桶是否已经包含了期望的节点f
                if (tabAt(tab, i) == f) {
                    // 能进来说明包含了,索引下标i的桶存储的就是期望的节点f
                    // tabAt(tab, i) == f 证明是正常情况,索引下标i的桶的对象没被其他线程修改更换
                    // 前面fh = f.hash, 所以fh记录的是f的哈希值
                    // static final int MOVED     = -1;   代表当前hash位置的数据正在扩容!
		   			// static final int TREEBIN   = -2;   代表当前hash位置下挂载的是一个红黑树
		   			// static final int RESERVED  = -3;   预留当前索引位置……
                    // 判断一手fh是不是大于0,也就是排除上面的三种情况
                    if (fh >= 0) {
                        // binCount是用来记录链表下面挂了几个
                        binCount = 1;
                        // 遍历下标i对应的桶下的链表,每遍历一次,binCount+1
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 看到这里或许有点忘记了,这个hash就是要存的键值对的Key计算出来的二次哈希值
                            // 判断一下数组下标i的hash与需要存的键值对的hash是否一样,表示判断是否是重复数据
                            if (e.hash == hash &&
                               	// 判断一手要存的键值对的Key与数组下标i的Key是不是同一个
                            	// 只要地址或内容有一个一样 说明就是同一个key
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                // 将老数据赋值给oldVal
                                oldVal = e.val;
                                // onlyIfAbsent就是put方法里面调用putVal方法里面的布尔值参数
                                // 如果为false 则新数据覆盖旧数据
                                // 如果为true 则不做任何处理
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 能走到这里,就代表了要存储的键值对,与当前遍历的Node节点记录的Key不是同一个
                            // pred记录当前的Node节点
                            Node<K,V> pred = e;
                            // e记录挂在e下的一个Node节点
                            // 判断一下e是不是为null 如果不为null 说明pred下面还有一个节点
                            // 那么继续走循环 继续判断是不是同一个Key 用不用覆盖数据
                            if ((e = e.next) == null) {
                                // 当走到最后一个Key都不是同一个的话,那么就创建一个Node节点挂上去
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 走到这个else-if说明fh >= 0不成立
                    // 那么判断一手当前下标i挂的是不是红黑树
                    else if (f instanceof TreeBin) {
                        // 如果是红黑树,就需要将数据插入进红黑树中
                        Node<K,V> p;
                        // 这个就有意思了,前面将数据插入链表的时候binCount初始化为1的
                        // 将数据插入红黑树的时候,binCount却初始化为2
                        // 这个暂时没想懂 后续懂了再补充
                        binCount = 2;
                        // 将Key-value放进红黑树中
                        // putTreeVal方法 如果返回null则代表添加
                        // 否则代表查找, 返回Key一样的节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            // 返回的p不为null 说明存在一样的Key
                            // 记录Key对应Value的旧值
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                // 覆盖数据
                                p.val = value;
                        }
                    }
                }
            }
            // 到这里,就完成了数据的插入
            // 到这一步,就是大家都熟悉的扩容或是链表转化为红黑树的操作了
            if (binCount != 0) {
                // binCount不为0,说明下标i对应的桶下的节点总数不等于0
                if (binCount >= TREEIFY_THRESHOLD)
                    // 节点总数大于等于8, 可能进行扩容,也可能进行链表转化红黑树
                    // 这个方法后面再说
                    treeifyBin(tab, i);
                if (oldVal != null)
                    // oldVal记录的是Key一样的情况下 旧的Value值
                    // 如果存在Key一样的情况下,那么就将旧的value值返回
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

二次哈希——spread方法

// 方法入参参数为Key的哈希值
// 在这个方法中,首先Key的哈希值h先要自身哈希值的高16位进行^(异或操作,相同为0,不同为1)
// 为什么要进行^操作??
// 原因是在后面的(n-1)&hash的操作计算索引下标的时候
// 00000000 00000000 00000000 01010101
// 00000000 00000000 00000000 00011111
// 可以看见,由于n的数值较小,高16位根本不参与运算,于是设计HasMap的作者就想出了二次哈希
// 就是将低16位与高16位进行^操作,综合高位数据,让哈希值分布更加均匀,减少哈希冲突

// 那么为什么低16位^高16位的计算结果要和HASH_BITS进行&(与运算,只有都为1的时候才为1)?
// 首先HASH_BITS的取值为0x7fffffff,这个值就是int的最大值 也就是01111111111111111111111111111111
// 而Key的哈希值也为int,所以哈希值的最大值也是0x7fffffff
// (h ^ (h >>> 16))完成后可能会导致进位,也就是位数超出32位
// 因此需要和HASH_BITS进行与操作,将哈希值的取值范围控制在32位,也就是将高位屏蔽
// 这样就能在下次(n-1)&hash提高运行效率
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

初始化table方法——initTable方法

private transient volatile int sizeCtl;
// sizeCtl: 表初始化和调整大小控件
// sizeCtl < 0: 表正在初始化或调整大小
//			 -1: 表示数组正在初始化
//         < -1: 低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2,如果2个线程扩容,值为-3)
// sizeCtl = 0: 代表数组还没初始化
// sizeCtl > 0: 代表当前数组扩容阈值, 或者是当前数组的初始化大小
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 初始化未完成时,一直进行while循环
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 小于0代表其他线程正在初始化,线程等待一下继续while循环
            Thread.yield(); // lost initialization race; just spin
        // 进行CAS修改
        // compareAndSwapInt方法
		// 		参数var1:表示要操作的对象本身;
		// 		参数var2:表示要操作对象中内存地址的偏移量;
		// 		参数var3:表示需要修改数据的期望的值;
		// 		参数var4:表示需要修改为的新值;
        // 线程安全,确保只有一个线程初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 更新成功之后,还需要判断一手
                // 防止重复初始化table,因为可能其他线程已经完成了table的初始化
                if ((tab = table) == null || tab.length == 0) {
                    // 如果table初始化还未完成,那么久进行table初始化
                    // sc记录的是sizeCtl更新为-1之前的值
                    // 		sc > 0: 代表当前数组扩容阈值, 或者是当前数组的初始化大小
                    // 		sc < 0: 则取默认扩容容量 16
                    //	默认使用无参构造方法的时候,默认扩容容量为16	
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 创建一个Node数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // table指向初始化的Node数组
                    table = tab = nt;
                    // 这个就是负载因子的由来
                    // 首先 n >>> 2 是将n的二进制向右移动两位
                    // 无论是构造方法指定容量还是使用DEFAULT_CAPACITY,n都是2的次幂
                    // 那么 n>>>2 就是等同于将n÷4
                    // 因此 sc = 0.75n
                    // 0.75n > 0 根据前面的 sizeCtl 的定义
                    // 此刻0.75n代表了数组扩容阈值
                    // 也就是说当容量达到0.75n的时候进行扩容
                    sc = n - (n >>> 2);
                }
            } finally {
                // 将上面求得的扩容阈值赋值给sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

CAS 返回table某个下标的Node——tabAt方法

// tab指向的是table table是被volatile修饰的
// 使用Unsafe类的getObjectVolatile方法获取索引下标的对象值
// getObjectVolatile方法第一个参数为获取值的对象 第二个参数为对象的内存偏移量,表示要访问对象的具体字段或数组元素位置。
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

利用CAS往table数组的某个下标插入值——casTabAt方法

// 利用Unsafe类的compareAndSwapObject方法 将table数组的某个下标对应值替换成需要存储的键值对
// compareAndSwapObject方法
//		第一个参数为需要操作的对象
//		第二个参数为对象的内存偏移量,表示要访问对象的具体字段或数组元素位置。
//		第三个参数为期望的值,用于比较对象当前的值。
//		第四个参数为要设置的新值,如果对象的当前值与期望值相等,则将新值设置到对象上。
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

5. 取值流程

对于取值,通常都是通过get方法根据Key取值

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算Key的二次哈希值
    int h = spread(key.hashCode());
    // table已经初始化 并且 table长度大于0 并且 Key的二次哈希值计算出的索引下标的桶中有值才进去找
    // 否则直接return null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            // 桶下挂的一个节点的哈希值与Key的二次哈希值一样
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                // 并且当Key的地址或Key的内容一样 则说明这就是Key对应的Value
                return e.val;
        }
        else if (eh < 0)
           	// static final int MOVED     = -1;   代表当前hash位置的数据正在扩容!
		  	// static final int TREEBIN   = -2;   代表当前hash位置下挂载的是一个红黑树
		   	// static final int RESERVED  = -3;   预留当前索引位置……
            // 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;
}
// Node内部的实现的find, 用于支持get方法
// 由于桶中可能包含链表或红黑树结构,因此需要根据情况进行不同的查找方式
// 当桶中的节点数量较多,且已经转换为红黑树时,会调用红黑树节点的 find 方法来进行查找,以保证查找效率
Node<K,V> find(int h, Object k) {
    Node<K,V> e = this;
    if (k != null) {
        do {
            K ek;
            if (e.hash == h &&
                ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
        } while ((e = e.next) != null);
    }
    return null;
}

6. 扩容流程

ConcurrentHashMap的扩容实现是要比HashMap复杂的。

ConcurrentHashMap的应用场景是多线程场景,需要综合考虑多线程对扩容产生的影响,避免HashMap在多线程情况下扩容出现了死链或数据错乱的问题。

触发扩容机制的触发,主要涉及两个方法``treeifyBintryPresize`方法

  • treeifyBin方法: 在putVal方法的时候,将一个键值对放进桶中,当链表长度大于等于8时,如果数组长度小于64,会调用treeifyBin方法进行扩容
  • tryPresize方法: 针对putAll或将Map作为构造参数public ConcurrentHashMap(Map<? extends K, ? extends V> m) 时候会可能触发的tryPresize方法进行扩容

这个扩容流程有点还没捋清楚,下一章再更新吧~

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

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

相关文章

【蓝桥杯省赛真题48】Scratch放大镜游戏 蓝桥杯scratch图形化编程 中小学生蓝桥杯省赛真题讲解

目录 scratch放大镜游戏 一、题目要求 编程实现 二、案例分析 1、角色分析

8.AUTOSAR 诊断栈分析(一)

目录 1.错误分级分类 2.错误上报方法 2.1 API上报 2.2 预定义的Callout上报 2.3 DET(Default Error Tracer)相关Hook或者Callout上报 2.4 DEM相关错误处理 2.5 DLT相关错误处理 3.小结 终于来到了整个ECU的核心&#xff1a;诊断Dianostic。 为了更加系统地了解诊断…

循环队列的实现(附完整代码)

题目解读 本题是要求我们设计一个循环的队列&#xff0c;循环队列要有以下功能&#xff1a; 1.获取队首元素&#xff0c;若队列为空返回-1 2.获取队尾元素&#xff0c;若队列为空&#xff0c;则返回-1 3.插入元素&#xff0c;插入成功返回真 4.删除元素&#xff0c;删除成功返回…

文件夹重命名:如何在文件夹名称左边插入关键字,简单步骤轻松完成

在电脑中管理文件时&#xff0c;经常需要对文件夹进行重命名&#xff0c;以便于更方便地查找和识别文件。有时候&#xff0c;需要在文件夹名称的左边插入一些关键字&#xff0c;通过重命名文件夹&#xff0c;可以更清晰、更准确地描述文件夹的内容&#xff0c;以便于更好地管理…

【数据结构】树与二叉树(廿四):树搜索给定结点的父亲(算法FindFather)

文章目录 5.3.1 树的存储结构5. 左儿子右兄弟链接结构 5.3.2 获取结点的算法1. 获取大儿子、大兄弟结点2. 搜索给定结点的父亲a. 算法FindFatherb. 算法解析c. 代码实现 3. 代码整合 5.3.1 树的存储结构 5. 左儿子右兄弟链接结构 【数据结构】树与二叉树&#xff08;十九&…

vivado产生报告阅读分析20-QOR

Report QoR Suggestions report_qor_suggestions 命令是处理 QoR 建议对象时使用的主要命令。 QoR 建议对象会创建命令和属性来改善设计的时序性能&#xff08; 欲知详情 &#xff0c; 请参阅 QoR 建议 &#xff09; 。 report_qor_suggestions 命令可执行两项任务 &am…

MFC添加窗体菜单栏和消息响应

在资源视图右键,添加资源,选择Menu,新建 添加的菜单在资源菜单的Menu目录下 双击直接编辑输入菜单 之后在要添加菜单的窗体的属性Menu里面填写菜单的ID就可以了 如何给菜单添加点击响应? OnCommand是MFC中的一个消息处理函数,用于处理在窗口或控件被激活时发出的WM_CO…

中伟视界:AI智能分析盒子实现全方位人车监测,保障管道安全

在油气管道长又无人的场景下&#xff0c;人和车的监测问题一直是一个难题。传统的监测手段往往存在盲区和误报问题&#xff0c;给管道运行安全带来了一定的隐患。然而&#xff0c;随着人工智能技术的不断发展&#xff0c;利用AI盒子的智能分析算法可以有效解决这一问题。 首先&…

python多线程为什么没有跑满CPU?

1、实验环境 Vmvare虚拟机&#xff1a;单处理器2核。 2、Python获取CPU使用率 import psutildef get_cpu_percent():cpu_percent psutil.cpu_percent(interval1)return cpu_percentwhile(1):cpu_percent get_cpu_percent()print("当前CPU占用率&#xff1a;{}%"…

PyCharm 安装插件Vue

一、打开PyCharm工具 File -> Settings -> Plugins 二、在项目中添加Vue.js的依赖项。 npm install vue 三、页面应用

我的第一个Arduino点灯程序

我简直难以相信&#xff0c;什么都不用配置&#xff0c;就这么几行代码&#xff0c;就可以blink了 void setup() {// Set up the built-in LED pin as an output:pinMode(PA1, OUTPUT); }void loop() {digitalWrite(PA1,!digitalRead(PA1));// Turn the LED from off to on, o…

3. 深入探究文件 IO

3. 深入探究文件 IO 1. Linux 系统如何管理文件1.1 静态文件与inode1.2 文件打开时的状态 2. 返回错误处理与errno2.1 strerror 函数2.2 perror 函数 3. 空洞文件4. O_APPEND 和 O_TRUNC5. 多次打开同一个文件6. 复制文件描述符6.1 dup6.2 dup2 7. 文件共享7.1 同一个进程中多次…

接口测试场景:怎么实现登录之后,需要进行昵称修改?

在接口测试中有一个这样的场景&#xff1a;登录之后&#xff0c;需要进行昵称修改&#xff0c;怎么实现&#xff1f; 首先我们分别看下登录、昵称修改的接口说明&#xff1a; 以上业务中补充一点&#xff0c;昵称修改&#xff0c;还需要添加请求头Authorization传登录获取的to…

Depends 下载

查看某个应用程序和动态库的依赖 属性查看是只支持WIN32 的&#xff0c;查看X64的动态库电脑会卡死 左边框可以查看动态库的依赖&#xff0c;右边 可以查看动态库的导出情况 链接&#xff1a;https://pan.baidu.com/s/1vUFrOuzTO_dfvvkHP0-UiQ 提取码&#xff1a;i09s

2. 流程控制|方法|数组|二维数组|递归

文章目录 流程控制代码块选择结构循环结构跳转控制关键字 方法方法的概述方法的重载Junit单元测试初识全限定类名 Debug 小技巧数组数组的基本概念数组的基本使用数组的声明数组的初始化 JVM内存模型什么是引用数据类型基本数据类型和引用数据类型的区别堆和栈中内容的区别 数组…

yolo系列中的一些评价指标说明

文章目录 一. 混淆矩阵二. 准确度(Accuracy)三. 精确度(Precision)四. 召回率(Recall)五. F1-score六. P-R曲线七. AP八. mAP九. mAP0.5十. mAP[0.5:0.95] 一. 混淆矩阵 TP (True positives)&#xff1a;被正确地划分为正例的个数&#xff0c;即实际为正例且被分类器划分为正例…

ModuleNotFoundError: No module named ‘torch_sparse‘

1、卸载 先把torch-geometric、torch-sparse、torch-scatter、torch-cluster、 torch-spline-conv全部卸载了 pip uninstall torch-geometric torch-scatter torch-sparse torch-cluster torch-spline-conv 2.conda list确定PyTorch的版本&#xff0c;我的是1.10 3、确定下载地…

《斯坦福数据挖掘教程·第三版》读书笔记(英文版)Chapter 3 Finding Similar Items

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT It is therefore a pleasant surprise to learn of a family of techniques called locality-sensitive hashing, or LSH, that allows us to focus on pairs that are likely to be similar, without hav…

使用bard分析视频内容

11月21日的bard update 更新了分析视频的功能&#xff0c;使用方法如下&#xff1a; 1、打开bard网站。https://bard.google.com/ 2、点击插件。 3、点击YouTube中的 research a topic 选项。 4、输入需要分析的内容&#xff1a; Please analyze how many technologies are in…

五种多目标优化算法(MOGWO、MOLPB、MOJS、NSGA3、MOPSO)求解微电网多目标优化调度(MATLAB代码)

一、多目标优化算法简介 &#xff08;1&#xff09;多目标灰狼优化算法MOGWO 多目标应用&#xff1a;基于多目标灰狼优化算法MOGWO求解微电网多目标优化调度&#xff08;MATLAB代码&#xff09;-CSDN博客 &#xff08;2&#xff09;多目标学习者行为优化算法MOLPB 多目标学习…