【JavaEE精炼宝库】多线程进阶(1)常见锁策略 | CAS | ABA问题

news2024/12/23 13:44:51

目录

一、常见的锁策略:

1.1 悲观锁 | 乐观锁:

1.2 重量级锁 | 轻量级锁:

1.3 自旋锁 | 挂起等待锁:

1.4 公平锁 | 非公平锁:

1.5 可重入锁 | 不可重入锁:

1.6 互斥锁 | 读写锁:

1.7 面试题:

二、CAS

2.1 CAS 的概念:

2.2 CAS 的实现的:

2.3 CAS 的应用:

2.3.1 实现原子类:

2.3.2 实现自旋锁:

2.4 CAS 的 ABA 问题:

2.4.1 ABA 问题的概述:

2.4.2 ABA 问题引来的 BUG:

2.5 解决方案:

2.6 面试题:


终于进入到多线程的进阶了,这里面涉及到的内容面试容易考,但是工作中很少直接用到。

一、常见的锁策略:

注意:接下来讲解的锁策略不仅仅是局限于 Java 。任何和 "锁" 相关的话题,都可能会涉及到以下内容。这些特性主要是给锁的实现者来参考的。我们了解一些,也能更加合理的使用锁。

1.1 悲观锁 | 乐观锁:

加锁的时候,预测当前锁冲突的概率是大还是小。

• 悲观锁:

预测当前锁的冲突概率大,后续要做的工作往往就会更多。加锁的开销就会更大(时间,系统资源)。

• 乐观锁:

预测当前锁的冲突概率不大,后续要做的工作往往就会更少。加锁的开销就会更小(时间,系统资源)。

synchronized 初始使用乐观锁策略。当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。所以 synchronized 既是乐观锁也是悲观锁,支持自适应。

1.2 重量级锁 | 轻量级锁:

一般来说,悲观锁往往就是重量级锁(加锁过程做的事情多),乐观锁往往就是轻量级锁(加锁过程做的事情少)。

锁的核心特性 "原子性",这样的机制追根溯源是 CPU 这样的硬件设备提供的。硬件有提供,软件层面才能实现。

• CPU 提供了 "原子操作指令"。

• 操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁。

• JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类。

注意:synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其他的工作。  

• 重量级锁: 

加锁机制重度依赖了 OS 提供了 mutex。

这样做的特点有:1. 大量的内核态用户态切换。2. 很容易引发线程的调度。

这两个操作,成本比较高,一旦涉及到用户态和内核态的切换,就意味着 “沧海桑田”。 

• 轻量级锁:

加锁机制尽可能不使用 mutex,而是尽量在用户态代码完成,实在搞不定了,再使用 mutex。

这样做的特点有:1. 少量的内核态用户态切换。2. 不太容易引发线程调度。

为什么会有这样的好处呢?举个栗子:

想象去银行办业务。在窗口外,自己做,这是用户态,用户态的时间成本是比较可控的。在窗口内让工作人员做,这是内核态,内核态的时间成本是不太可控的(可能人家处理一半,去做别的事情了)。如果办业务的时候反复和工作人员沟通,还需要重新排队,这时效率是很低的。

重量级锁、轻量级锁和悲观锁、乐观锁的概念有重合的地方,面试的时候要能转的过来。

synchronized 开始是一个轻量级锁。如果锁冲突严重,就会变成重量级锁。

1.3 自旋锁 | 挂起等待锁:

• 自旋锁:

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题。

自旋锁伪代码:

while (抢锁(lock) == 失败) {}

⼀旦锁被其他线程释放,就能第⼀时间获取到锁(线程没有被调度)。

自旋锁是一种典型的轻量级锁的实现方式。

优点:没有放弃 CPU,不涉及线程阻塞和调度,⼀旦锁被释放,就能第一时间获取到锁。

缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源,CPU 在空转。

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。

• 挂起等待锁:

是重量级锁的一种典型的实现方式,借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(阻塞状态)。此时这个线程就不会参与线程调度了。知道这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁。

1.4 公平锁 | 非公平锁:

• 公平锁:遵守 "先来后到"。B 比 C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。

• 非公平锁:不遵守 "先来后到"。B 和 C 都有可能获取到锁。

其实这两个策略都挺公平的,只是最初的 Java 大佬把先来后到定义成公平,均等机会定义成不公平。

注意:

• 操作系统内部的线程调度就可以视为是随机的。如果不做任何额外的限制,锁就是非公平锁。如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序。

• 公平锁和非公平锁没有好坏之分,关键还是看业务场景。

synchronized 非公平锁。

1.5 可重入锁 | 不可重入锁:

可重入锁的字面意思是 “可以重新进入的锁”,即允许同一个线程多次获取同一把锁。

例如:一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。 

Java里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁。synchronized 是可重入锁。这个前面几篇文章有涉及到,这里就不再赘述。

1.6 互斥锁 | 读写锁:

我们平时见到的 synchronized 是普通的互斥锁,读写锁是更加特殊的存在。

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

Java 的读写锁是这样设定的:

• 读锁和读锁之间,不会产生互斥。

• 写锁和写锁之间,会产生互斥。

• 读锁和写锁之间,会产生互斥。

突出体现的是 “读操作和读操作” 之间是共享的(不会互斥),有利于降低锁冲突的概率,提高并发能力。

注意:和之前谈到的数据库中的事务,给读操作加锁:读的时候不能写。给写操作加锁:写的时候不能读。不是一回事。这是在减低并发能力。

读写锁就是把读操作和写操作区分对待。Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁。

• ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。

• ReentrantReadWriteLock.WriteLock 类表示一个写锁。这个对象也提供了 lock / unlock 方法进行加锁解锁。

读写锁特别适合于 "频繁读,不频繁写" 的业务中。(这样的场景其实也是非常广泛存在的)。

1.7 面试题:

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

答:悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁。乐观锁认为多个线程访问同⼀个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突。悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex)获取到锁再操作数据。获取不到锁就等待。乐观锁的实现可以引入一个版本号。借助版本号识别出当前的数据访问是否冲突。

2. 介绍下读写锁?

答: 读写锁就是把读操作和写操作分别进行加锁。读锁和读锁之间不互斥。写锁和写锁之间互斥,写锁和读锁之间互斥,读写锁最主要用在 "频繁读,不频繁写" 的场景中。

3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

答:如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试,会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。

相比于挂起等待锁:

• 优点:没有放弃 CPU 资源,一旦锁被释放就能第一时间获取到锁,更高效。在锁持有时间比较短的场景下非常有用。

• 缺点:如果锁的持有时间较长,就会浪费 CPU 资源。

4. synchronized 是可重入锁么?

答:是可重入锁。可重入锁指的就是连续两次加锁不会导致死锁。实现的方式是在锁中记录该锁持有的线程身份,以及⼀个计数器(记录加锁次数),如果发现当前加锁的线程就是持有锁的线程,则直接计数自增。

二、CAS

2.1 CAS 的概念:

CAS:全称Compare and swap,字面意思:”比较并交换“。一个 CAS 涉及到以下操作:我们假设内存中的原数据V,旧的预期值A,需要修改成的新值B。

1. 比较 A 与 V 是否相等。(比较)

2. 如果比较相等,将 B 写入 V。(交换)

3. 返回操作是否成功。

这是一条 CPU 指令(原子的),可以完成比较和交换。这给我们编写线程安全的代码,打开了新世界的大门。

• CAS 伪代码:

注意:下面写的代码不是原子的,真实的 CAS 是⼀个原子的硬件指令完成的。这个伪代码只是辅助理解 CAS 的工作流程。

address 是内存地址,expectValue 和 swapValue 都是寄存器的值(CPU)。 

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
 &address = swapValue;
 return true;
 }
 return false;
}

当多个线程同时对某个资源进行 CAS 操作,只能有一个线程能操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS 可以视为是一种乐观锁。(或者可以理解成 CAS 是乐观锁的一种实现方式)

2.2 CAS 的实现的:

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

• java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作。

• unsafe 的 CAS 依赖的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg。

• Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 CPU 硬件提供的 lock 机制保证其原原性。

简而言之,是因为硬件予以了支持,软件层面才能做到。

2.3 CAS 的应用:

基本涉及到锁,程序就和高性能无缘了。这里可以为无锁编程提供一些思路(当然大部分情况下,只有加锁才行)。

2.3.1 实现原子类:

标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类。

如下案例:

import java.util.concurrent.atomic.*;
public class demo1 {
    static AtomicInteger count = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 50000;i++){
                    count.getAndIncrement();
                }
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

案例演示结果如下:

可以发现是线程安全的。因为这里的 ++ 操作是原子的。

伪代码实现:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

• 对上面代码执行过程的刨析:

假设两个线程同时调用 getAndIncrement:

1. 两个线程都读取 value 的值到 oldValue 中。(oldValue 是⼀个局部变量,在栈上,每个线程有自己的栈)。

2. 线程 1 先执行 CAS 操作。由于 oldValue 和 value 的值相同,直接进行对 value 赋值。

注意:CAS 是直接读写内存的,而不是操作寄存器。 CAS 的读内存,比较,写内存操作是⼀条硬件指令,是原子的。

3. 线程 2 再执行 CAS 操作,第一次 CAS 的时候发现 oldValue 和 value 不相等,不能进行赋值。因此需要进入循环。在循环里重新读取 value 的值赋给 oldValue。

4. 线程 2 接下来第二次执行 CAS,此时 oldValue 和 value 相同,于是直接执行赋值操作。

5. 线程 1 和线程 2 返回各自的 oldValue 的值即可。

通过形如上述代码就可以实现⼀个原子类。不需要使用重量级锁,就可以高效的完成多线程的自增操作。

2.3.2 实现自旋锁:

• 自旋锁伪代码:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.  
        // 如果这个锁已经被别的线程持有, 那么就⾃旋等待.  
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.  
        while(!CAS(this.owner, null, Thread.currentThread())){
        }
    }
    public void unlock (){
        this.owner = null;
    }
}

2.4 CAS 的 ABA 问题:

2.4.1 ABA 问题的概述:

假设存在两个线程 t1 和 t2。有一个共享变量 num,初始值为 A。接下来,线程 t1 想使用CAS 把 num 值改成 Z,那么就需要先读取 num 的值,记录到 oldNum 变量中。使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。

但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A。

到了这里就有个问题:线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了。只不过又改成 A 了。这个时候 t1 究竟是否要更新 num 的值为 Z ?

 

这就好比,我们买一个手机,无法判定这个手机是刚出厂的新手机,还是别人用旧了,又翻新过的手机。

2.4.2 ABA 问题引来的 BUG:

大部分的情况下,t2 线程这样的一个反复横跳改动,对于 t1 是否修改 num 是没有影响的。但是不排除一些特殊情况。

案例:假设滑稽有 100 存款。滑稽想从 ATM 取 50 块钱。取款机创建了两个线程,并发的来执行 -50 操作。我们期望一个线程执行 -50 成功,另一个线程 -50 失败。如果使用 CAS 的方式来完成这个扣款过程就可能出现问题。

• 正常的过程:

存款 100,线程 1 获取到当前存款值为 100,期望更新为 50。线程 2 获取到当前存款为 100,期望更新为 50。 线程 1 执行扣款成功,存款被改成 50。线程 2 阻塞等待中。轮到线程 2 执行,发现当前存款为 50,和之前读到的 100 不相同,执行失败。

• 异常的过程:

存款 100。线程 1 获取到当前存款值为 100,期望更新为 50。线程 2 获取到当前存款为 100,期望更新为 50。线程 1 执行扣款成功,存款被改成 50。线程 2 阻塞等待中。在线程 2 执行之前,滑稽的朋友正好给滑稽转账 50,账户余额变成 100。轮到线程 2 执行了,发现当前存款为 100,和之前读到的 100 相同,再次执行扣款操作,这个时候,扣款操作被执行了两次。这就是是 ABA 问题搞的鬼。

2.5 解决方案:

给要修改的值,引入版本号(约定版本号只能加,不能减,每次操作一次余额,版本号都要 + 1)。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。CAS 操作在读取旧值的同时,也要读取版本号。真正修改的时候,如果当前版本号和读到的版本号相同,则修改数据,并把版本号 +1。如果当前版本号高于读到的版本号。就操作失败(认为数据已经被修改过了)。

可以看到:如果数据本身属于 ”能加也能减“,就容易出现 ABA 问题。

2.6 面试题:

1. 讲解下你自己理解的 CAS 机制:

全称 Compare and swap,即"比较并交换"。相当于通过⼀个原子的操作,同时完成 "读取内存,比较是否相等,修改内存" 这三个步骤。本质上需要 CPU 指令的支撑。

2. ABA问题怎么解决?

给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增。如果发现当前版本号比之前读到的版本号大,就认为操作失败。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

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

相关文章

Web前端第四次作业

目录 一、编写一个函数&#xff0c;形参是一个数组&#xff0c;返回数组中所有数字的平均值 二、编写一个函数&#xff0c;形参是一个数组&#xff0c;返回数组中的最大值 三、编写一个函数&#xff0c;形参是一个字符串&#xff0c;统计该字符串中每个字母出现的次数&#…

大模型微调方法总结

一 LoRA&#xff1a; 1 低(秩)rank 自适应微调方法 2 背景及本质   大模型的参数更新耗费大量现存为此&#xff0c; 微软的研究者们于2021年通过论文《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》提出了低秩适应LoRA 它冻结了预训练的模型权重&#xff0c;并将可…

2005年下半年软件设计师【下午题】试题及答案

文章目录 2005年下半年软件设计师下午题--试题2005年下半年软件设计师下午题--答案 2005年下半年软件设计师下午题–试题 2005年下半年软件设计师下午题–答案

AI绘画Stable Diffusion人物背景替换实操教程,让创意无限延伸

大家好&#xff0c;我是画画的小强 Stable Diffusion以其强大的能力可以实现人物背景的更换。本文将带你深入了解如何利用Stable Diffusion中的Inpaint Anything插件快速且精准地实现人物背景的替换&#xff0c;从而让你的图片焕发新生。 前期准备 本文会使用到Inpaint Anyt…

以敏感数据保护为中心,建立健全高校数据安全治理体系

教育行业数据安全事件频发 2023年8月&#xff0c;南昌某高校3万余条师生个人信息数据在境外互联网上被公开售卖&#xff0c;该校受到责令改正、警告并处80万元人民币罚款的处罚&#xff0c;主要责任人被罚款5万元人民币。2023 年 7月&#xff0c;中国人民大学一名毕业生马某某…

ClickHouse vs. Elasticsearch:十亿行数据的较量

本文字数&#xff1a;15291&#xff1b;估计阅读时间&#xff1a;39 分钟 审校&#xff1a;庄晓东&#xff08;魏庄&#xff09; 本文在公众号【ClickHouseInc】首发 Meetup活动 ClickHouse 上海首届 Meetup 讲师招募中&#xff0c;欢迎讲师在文末扫码报名&#xff01; 引言 这…

2024上海初中生古诗文大会倒计时4个月:单选题真题示例和独家解析

现在距离2024年初中生古诗文大会还有4个多月时间&#xff0c;我们继续来看10道选择题真题和详细解析&#xff0c;以下题目截取自我独家制作的在线真题集&#xff0c;都是来自于历届真题&#xff0c;去重、合并后&#xff0c;每道题都有参考答案和解析。 为帮助孩子自测和练习&…

Chromium下载

https://chromium.woolyss.com/download/ https://www.chromium.org/getting-involved/download-chromium/

力扣SQL50 即时食物配送 II min函数 嵌套查询

Problem: 1174. 即时食物配送 II &#x1f468;‍&#x1f3eb; 参考题解 Code -- 计算立即配送的订单百分比 select round (-- 计算订单日期与客户偏好配送日期相同的订单数量sum(case when order_date customer_pref_delivery_date then 1 else 0 end) * 100 /-- 计算总订…

Studying-代码随想录训练营day19| 530.二叉搜索树的最小绝对差、501.二叉搜索树中的众数、236.二叉树的最近公共祖先

第十九天&#xff0c;二叉树part06&#xff0c;二叉树的道路任重而道远&#x1f4aa; 目录 530.二叉搜索树的最小绝对差 501.二叉搜索树中的众数 236.二叉树的最近公共祖先 总结 530.二叉搜索树的最小绝对差 文档讲解&#xff1a;代码随想录二叉搜索树的最小绝对差 视频…

2024年高性价比蓝牙耳机怎么买?揭秘超高性价比蓝牙耳机推荐

当一打开购物软件想购买一款性价比高的蓝牙耳机&#xff0c;你就会发现&#xff0c;不同的蓝牙耳机的品牌价格差距蛮大的&#xff01;几十块的随处可见&#xff0c;上千块的也琳琅满目&#xff0c;品牌和款式更是繁多&#xff0c;让人看得眼花缭乱&#xff0c;无从下手......其…

浅谈逻辑控制器之ForEach控制器

浅谈逻辑控制器之ForEach控制器 ForEach控制器是一个非常实用的功能&#xff0c;它允许用户遍历某个变量的所有值&#xff0c;并为每个值执行控制器内的子采样器或逻辑。这对于处理从先前请求&#xff08;如CSV Data Set Config、JSON Extractor、Regular Expression Extracto…

银幕光影交织,红酒香醇流淌,一场电影与红酒的绝美浪漫邂逅

在光影交错的世界里&#xff0c;红酒与电影总能在不经意间碰撞出浪漫的火花。当银幕上的角色轻启瓶盖&#xff0c;那迷人的酒香便如诗如画般弥漫开来&#xff0c;与影片的情节交织在一起&#xff0c;构成了一幅幅动人的画面。今天&#xff0c;就让我们一起走进这个充满酒香的银…

9.XSS之过滤

XSS之过滤 通过输入代码发现被过滤掉了 <script>";"666查看页面元素代码&#xff0c;被后台代码过滤了 尝试一下大小写混合使用&#xff0c;攻击代码如下&#xff1a; <sCRipT>alert(你打篮球像oldqu)</sCrIPt>如下图所示&#xff0c;大小写混…

1986-2017年 全国农村固定观察点数据

全国农村固定观察点调查体系是中国一个重要的农村社会经济调查项目&#xff0c;它通过长期跟踪调查固定不变的村庄和农户&#xff0c;收集连续性数据&#xff0c;以获取农村基层的动态信息。这些数据不仅全面反映了农户及其家庭成员在生产、消费、就业、生活等方面的情况&#…

05-java基础——循环习题

循环的选择&#xff1a;知道循环的次数或者知道循环的范围就使用for循环&#xff0c;其次再使用while循环 猜数字 程序自动生成一个1-100之间的随机数&#xff0c;在代码中使用键盘录入去猜出这个数字是多少&#xff1f; 要求&#xff1a;使用循环猜&#xff0c;一直猜中为止…

动态规划数字三角形模型——AcWing 1015. 摘花生

动态规划数字三角形模型 定义 动态规划数字三角形模型是在一个三角形的数阵中&#xff0c;通过一定规则找到从顶部到底部的最优路径或最优值。 运用情况 通常用于解决具有递推关系、需要在不同路径中做出选择以达到最优结果的问题。比如计算最短路径、最大和等 注意事项 …

OCR的有效数据增强

背景 我面临着需要尽可能准确识别手写金额的挑战。难点在于保持误判率低于0.01%。由于数据集中样本数量固定&#xff0c;因此数据增强是合乎逻辑的选择。快速搜索未发现针对光学字符识别&#xff08;OCR&#xff09;的现成方法。因此&#xff0c;我挽起袖子&#xff0c;亲自创建…

数据挖掘案例-商品零售购物篮分析

数据挖掘案例-商品零售购物篮分析 1. 背景与挖掘目标 现代商品种类繁多&#xff0c;顾客往往会由于需要购买的商品众多而变得疲于选择&#xff0c;且顾客并不会因为商品选择丰富而选择购买更多的商品。 例如&#xff0c;货架上有可口可乐和百事可乐&#xff0c;若顾客需要选…

「全新升级,性能更强大——ONLYOFFICE 桌面编辑器 8.1 深度评测」

文章目录 一、背景二、界面设计与用户体验三、主要新功能亮点3.1 高效协作处理3.2 共同编辑&#xff0c;毫无压力3.3 批注与提及3.4 追踪更改3.5 比较与合并3.6 管理版本历史 四、性能表现4.1 集成 AI 工具4.2 插件强化 五、用户反馈与使用案例 一、背景 Ascensio System SIA -…