ReentrantLock的源码实现和原理介绍

news2024/9/22 17:16:47

目录

一、概述

二、ReentrantLock的整体结构

三、ReentrantLock 和Synchronized相比

四、ReentrantLock 公平锁和非公平锁实现

4.1 ReentrantLock 源码解读

4.1.1 ReentrantLock 类源码解读

4.1.1.1 Lock接口

4.1.1.2 Sync抽象类

4.1.1.3 NonfairSync()和FairSync()

4.1.1.3.1 NonfairSync介绍

4.1.1.3.2 FairSync介绍

4.1.2 ReentrantLock涉及的AQS方法源码

4.1.2.1 acquire()

4.1.2.2 addWaite()

4.1.2.3 acquireQueued()

4.1.2.4 enq()

4.1.2.5 shouldParkAfterFailedAcquire()

4.2 公平锁、非公平锁流程

五、ReentrantLock 释放锁实现

5.1 释放锁源码

5.1.1 release()

5.1.2 unparkSuccessor()

5.2 释放锁流程


一、概述

ReentrantLock是一种基于AQS(抽象队列同步器)框架的应用实现,是Java提供的强大且灵活的可重入锁,支持公平和非公平特性。是JDK中一种线程并发访问的同步手段,它提供了与synchronized关键字相似的功能,但具有更多的灵活性和扩展性。

二、ReentrantLock的整体结构

ReentrantLock 实现了Lock通用接口,通过Sync继承了AQS特性,Syns下有两个实现,NonfairSyns和FairSync,分别实现的是非公平锁和公平锁功能,它的整体结构如下图所示:

三、ReentrantLock 和Synchronized相比

  • 在功能实现和原理上

首先Synchronized 实现的是公平锁,是基于对象锁实现的并发编程同步关键字;而ReentrantLock是lock接口实现,基于AQS+CAS实现,支持公平锁和非公平锁,相比Synchronized,lock锁提供的功能更完善,lock可以使用tryLock指定等待锁的时间;lock锁还提供了lockInterruptibly允许线程在获取锁的期间被中断。

  • 在使用上

Synchronized 是Java关键字,可以作用在方法上,也可以作用在代码块上,实现简单;对于并发编程掌握不好的编程人员来说比较友好。

ReentrantLock 的使用需要自己先初始化ReentrantLock,然后手动调用它的锁方法对同步代码加锁,使用完成后,需要在finally中调用释放锁方法释放。对于并发编程掌握不好的编程人员来说使用成本较高。而且可能极容易出错。

四、ReentrantLock 公平锁和非公平锁实现

4.1 ReentrantLock 源码解读

4.1.1 ReentrantLock 类源码解读

4.1.1.1 Lock接口

首先ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,接口的定义如下:

public interface Lock {

    // 获取锁
    void lock();

    // 获取锁--支持响应中断
    void lockInterruptibly() throws InterruptedException;

    //尝试加锁,返回是否成功状态
    boolean tryLock();

    // 尝试加锁,返回是否成功状态,支持指定加锁时间,超时响应中断
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 释放锁
    void unlock();

    // 条件变量
    Condition newCondition();
}
4.1.1.2 Sync抽象类

Sync继承了AbstractQueuedSynchronizer抽象接口,定义了抽象的lock方法,该方法需要子类自行实现,定义了一个nonfairTryAcquire非公平锁方法,定义了一个释放锁的tryRelease方法,它是ReentrantLock的核心类,具体源码和解释如下:

 // 获取锁,需要子类去实现
        abstract void lock();

        // 非公平锁获取state资源
        final boolean nonfairTryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前状态
            int c = getState();
            if (c == 0) { // state等于0,代表可加锁
                // 使用cas锁,尝试将state修改为acquires,acquires等于1
                if (compareAndSetState(0, acquires)) {
                    // 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
                    setExclusiveOwnerThread(current);
                    // 返回true状态
                    return true;
                }
            }

            else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
                // state+1,累加重入次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
                setState(nextc);
                // 返回true,表示成功
                return true;
            }
            // 返回false,表示失败
            return false;
        }

  // 释放锁
        protected final boolean tryRelease(int releases) {
            // 获取state状态值,然后减去releases,此处releases为1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果持有锁线程不是当前线程,抛出异常。
                throw new IllegalMonitorStateException();
            // 返回的状态值,默认为false
            boolean free = false;
            if (c == 0) { // 如果state-1后等于0,表示释放成功
                // 设置返回状态为true
                free = true;
                // 清空持有锁线程
                setExclusiveOwnerThread(null);
            }
            // 如果state-1后不等于0,说明当前持有锁线程是重入锁状态,需要设置相应的释放次数
            setState(c);
            return free;
        }
4.1.1.3 NonfairSync()和FairSync()

Lock接口定义的函数不多,接下来ReentrantLock要去实现这些函数,遵循着解耦可扩展设计,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync 和 FairSync是基于Sync扩展的子类,他们分别是ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。

4.1.1.3.1 NonfairSync介绍

在 ReentrantLock中支持两种获取锁的策略,分别是非公平策略与公平策略,NonfairSync 就是非公平策略。

此时大家会有问,什么是非公平策略?

在说非公平策略前,先简单的说下AQS(AbstractQueuedSynchronizer)流程,AQS为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,获取锁失败的线程,会进入CLH队列阻塞,其他线程解锁会唤醒CLH队列线程,如下图所示(简化流程):

接下来我们来解读下源码,看下NonfairSync是如何实现的

源码:

/**
     * 非公平锁实现
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        // 获取锁
        final void lock() {
            if (compareAndSetState(0, 1)) // 使用cas设置state状态为1,如果成功,代表加锁成功
                // 获取锁成功,设置当前持有锁的线程为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 获取锁失败,执行AQS获取锁的模板方法流程
                acquire(1);
        }

        // 获取锁,使用的Sync提供的nonfairTryAcquire方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

从源码我们可以知道,NonfairSync继承Sync,然后实现了lock()函数,lock()函数也非常简单,使用CAS设置状态值state为1,如果成功代表获取锁成功,否则执行AQS的acquire()函数( 获取锁模板 );另外NonfairSync还实现了AQS留给子类实现的tryAcquire()函数( 获取资源 ),函数直接使用Sync提供的nonfairTryAcquire()函数来实现tryAcquire(),最后子类实现的tryAcquire()函数在AQS的 acquire函数中被使用。

是不是有点绕?画个图给大家一起缕一缕:

首先 AQS 的acquire()函数是获取锁的流程模板,模板流程会先执行 tryAcquire()函数获取资源,tryAcquire()函数要子类实现,NonfairSync作为子类,实现了tryAcquire()函数,具体实现是调用了 Sync的 nonfairTryAcquire()函数。

我们接着看下nonfairTryAcquire() 的源码是如何实现的:

// 非公平锁获取state资源
        final boolean nonfairTryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前状态
            int c = getState();
            if (c == 0) { // state等于0,代表可加锁
                // 使用cas锁,尝试将state修改为acquires,acquires等于1
                if (compareAndSetState(0, acquires)) {
                    // 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
                    setExclusiveOwnerThread(current);
                    // 返回true状态
                    return true;
                }
            }

            else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
                // state+1,累加重入次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
                setState(nextc);
                // 返回true,表示成功
                return true;
            }
            // 返回false,表示失败
            return false;
        }

对上述代码逻辑我们做个简单的概括,当前线程查看资源是否可获取:

  • 可获取,尝试使用CAS设置state为 1,CAS成功代表获取资源成功,否则获取资源失败
  • 不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败

用下图说明一下流程:

4.1.1.3.2 FairSync介绍

有非公平策略,就有公平策略,FairSync 就是公平策略。

所谓公平策略就是,严格按照 CLH 队列顺序获取锁,线程释放锁时,会唤醒 CLH 队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH队列的线程参与竞争,为了保证公平,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败( 构建成节点插入到CLH队尾,由AQS模板流程执行 ),直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。

接下来我们来看下FairSync 公平锁的源码和解析

源码:


// 公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        // 获取锁
        final void lock() {
            // 执行AQS获取锁的模板方法流程
            acquire(1);
        }

        // 获取锁
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前state状态
            int c = getState();
            if (c == 0) { // state等于0,代表可加锁
                // hasQueuedPredecessors 判断当前线程是不是CLH队列中唤醒的线程
                // 使用cas锁,尝试将state修改为acquires,acquires等于1
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
            else if (current == getExclusiveOwnerThread()) {
                // state+1,累加重入次数
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
                setState(nextc);
                return true;
            }
            return false;
        }
    }

其实从上面的源码中我们不难发现 FairSync 的流程与 NonfairSync 基本一致,唯一的区别就是在 CAS执行前,多了一步调用hasQueuedPredecessors()函数,这一步就是判断当前线程是不是 CLH 队列被唤醒的线程,如果是就执行CAS操作,否则获取资源失败,具体流程图如下所示:

4.1.2 ReentrantLock涉及的AQS方法源码

4.1.2.1 acquire()

acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法,首先执行tryAcquire()函数,这个函数其实最终调用的是NonfairSync和FairSync里面的tryAcquire() 函数;在执行加锁不成功后,会将当前线程封装成Node节点加入到CLH队列中,具体源码和解析如下:

// 核心acquire     arg = 1
public final void acquire(int arg) {
    //1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,
    //   需要执行&&后面的方法

    //2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,
    //   并且插入到AQS的队列的末尾,并且作为tail

    //3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源
    //   如果没在前面,尝试将线程挂起,阻塞起来!
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
4.1.2.2 addWaite()

addWaite()函数,主要是将当前获取锁失败的线程,封装成功Node对象节点,加入到CLH队列中,具体源码和解析如下:

// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
    private Node addWaiter(Node mode) {
        // 将当前线程封装为Node对象,mode为null,代表互斥锁
        Node node = new Node(Thread.currentThread(), mode);
        // pred是tail节点
        Node pred = tail;

        if (pred != null) { // 如果pred不为null,有线程正在排队
            // 将当前节点的prev,指定tail尾节点
            node.prev = pred;
            // 以CAS的方式,将当前节点变为tail节点
            if (compareAndSetTail(pred, node)) {
                // 之前的tail的next指向当前节点
                pred.next = node;
                return node;
            }
        }

        // 添加的流程为,  自己prev指向、tail指向自己、前节点next指向我
        // 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列

        enq(node);
        return node;
    }
4.1.2.3 acquireQueued()

 // 查看当前排队的Node是否是head的next,
    // 如果是,尝试获取锁资源,
    // 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 拿到上一个节点
                final Node p = node.predecessor();
                //p == head 说明当前节点是head的next
                // tryAcquire 竞争锁资源,成功:true,失败:false
                if (p == head && tryAcquire(arg)) {
                    // 进来说明拿到锁资源成功
                    // 将当前节点置位head,thread和prev属性置位null

                    setHead(node);
                    p.next = null; // help GC
                    // 设置获取锁资源成功
                    failed = false;
                    // 不管线程中断。
                    return interrupted;
                }
                // 如果不是或者获取锁资源失败,尝试将线程挂起
                // shouldParkAfterFailedAcquire 当前节点的上一个节点的状态正常!
                // parkAndCheckInterrupt 挂起线程

                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
4.1.2.4 enq()

private Node enq(final Node node) {
        for (;;) {
            // 拿到tail
            Node t = tail;
            if (t == null) { // 如果tail为null,说明当前没有Node在队列中
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // 以CAS的方式,将当前节点变为tail节点
                if (compareAndSetTail(t, node)) {
                    // 之前的tail的next指向当前节点
                    t.next = node;
                    return t;
                }
            }
        }
    }
4.1.2.5 shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //拿到上一个节点的状态
        int ws = pred.waitStatus;
        // 如果上一个节点为 -1
        if (ws == Node.SIGNAL)
            // 返回true,挂起线程
            return true;
        if (ws > 0) { // 如果上一个节点是取消状态
            // 循环往前找,找到一个状态小于等于0的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将小于等于0的节点状态该为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

4.2 公平锁、非公平锁流程

经过上面的源码解读,我们基本上对ReentrantLock的公平锁、非公平锁有了一个而大概的了解,为了使我们理解更深刻,我们这里总结一下他们的加锁流程。

公平锁调用流程:

非公平锁调用流程:

公平锁、非公平锁整体加锁流程:

五、ReentrantLock 释放锁实现

5.1 释放锁源码

5.1.1 release()

public final boolean release(int arg) {
        // 核心的释放锁资源方法
        if (tryRelease(arg)) {
            Node h = head;
            // 释放锁资源释放干净了。  (state == 0)
            if (h != null && h.waitStatus != 0)
                // 唤醒线程
                unparkSuccessor(h);
            return true;
        }
        // 释放锁成功,但是state != 0
        return false;
    }

5.1.2 unparkSuccessor()

// 唤醒节点
    private void unparkSuccessor(Node node) {
        // 拿到头节点状态
        int ws = node.waitStatus;
        // 如果头节点状态小于0,换为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 拿到当前节点的next
        Node s = node.next;
        // 如果s == null ,或者s的状态为1
        if (s == null || s.waitStatus > 0) {
            // next节点不需要唤醒,需要唤醒next的next
            s = null;
            // 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 经过循环的获取,如果拿到状态正常的节点,并且不为null
        if (s != null)
            // 唤醒线程
            LockSupport.unpark(s.thread);
    }

为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?

因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。

如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~

如果从后往前找,肯定可以找到全部的节点。

5.2 释放锁流程

下面用一张图说明一下释放锁的流程:

今天ReentrantLock的源码实现和原理介绍的相关内容就分享到这里,如果帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

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

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

相关文章

《0基础》学习Python——第十讲

小知识点补充 一、json大字符串 JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;它以易于阅读和编写的方式为基础&#xff0c;同时也易于解析和生成。因为其简洁性和可读性&#xff0c;JSON已成为数据交换的首选格式。 大字符…

超大模型加载转换Trick

在深度学习领域&#xff0c;大模型的训练和推理通常需要消耗大量的计算和内存。如何高效地加载和使用大模型是一个相当关键的问题。在这篇博客中&#xff0c;我将分享一些关于更快加载大模型和减少内存的技巧。 1.问题分析 假设现在我们有一个236B 超大模型的原始权重的 check…

jmeter-beanshell学习9-放弃beanshell

写这篇时候道心不稳了&#xff0c;前面写了好几篇benashell元件&#xff0c;突然发现应该放弃。想回去改前面的文章&#xff0c;看了看无从下手&#xff0c;反正已经这样了&#xff0c;我淋了雨&#xff0c;那就希望别人也没有伞吧&#xff0c;哈哈哈哈&#xff0c;放在第九篇送…

DHCP原理及配置

目录 一、DHCP原理 DHCP介绍 DHCP工作原理 DHCP分配方式 工作原理 DHCP重新登录 DHCP优点 二、DHCP配置 一、DHCP原理 1 DHCP介绍 大家都知道&#xff0c;现在出门很多地方基本上都有WIFI&#xff0c;那么有没有想过这样一个问题&#xff0c;平时在家里都是“固定”的…

互联网十万个为什么之什么是专有网络VPC?

专有网络VPC有什么优势&#xff1f; 专有网络VPC具有安全可靠、灵活可控、简单易用的特性和较强的可扩展性。 安全可靠 每个VPC都有一个独立的隧道号&#xff0c;一个隧道号对应着一个虚拟化网络。VPC之间通过隧道号进行隔离&#xff1a; 由于VPC内部存在交换机和路由器&#…

PyTorch人脸识别

新书速览|PyTorch深度学习与企业级项目实战-CSDN博客 一套基本的人脸识别系统主要包含三部分&#xff1a;检测器、识别器和分类器&#xff0c;流程架构如图11-3所示&#xff1a; 图11-5 检测器负责检测图片中的人脸&#xff0c;再将检测出来的人脸感兴趣区域&#xff08;Reg…

如何在单片机外部Flash存储器上部署高效文件系统:从原理到实现

目录 1.Littlefs文件系统 1.1文件系统简介 2 Littlefs文件系统移植到单片机上 2.1 添加源代码 2.2 编辑接口函数 2.3 测试代码 1.Littlefs文件系统 1.1文件系统简介 littlefs文件系统源码下载地址&#xff1a;littlefs-project/littlefs: A little fail-safe filesystem…

Unity Shader学习笔记

Shader类型 类型详情Standard Surface Shader标准表面着色器&#xff0c;基于物理的着色系统&#xff0c;用于模拟各种材质效果&#xff0c;如石头、木材、玻璃、塑料和金属等。Unlit Shader最简单的着色器&#xff0c;不包含光照但包含雾效&#xff0c;只由最基础的Vertex Sh…

Pytorch使用Dataset加载数据

1、前言&#xff1a; 在阅读之前&#xff0c;需要配置好对应pytorch版本。 对于一般学习&#xff0c;使用cpu版本的即可。参考教程点我 导入pytorch包&#xff0c;使用如下命令即可。 import torch # 注意虽然叫pytorch&#xff0c;但是在引用时是引用torch2、神经网络获取…

【C++】—— 初识C++

【C】—— 初识C 一、什么是 C二、C 的发展历史三、C 版本更新四、C 的重要性五、C 在工作领域中的运用六、C 书籍推荐&#xff1a; 一、什么是 C C语言 是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的程序&#xff0c;需要…

六、STM32F4+标准库+LWIP2.1.2移植+无操作系统

最快最简单的移植LWIP协议栈&#xff0c;可改可不改的东西统一不修改。后期学会了有能力了再回过头来修改&#xff0c;操作复杂理论复杂&#xff0c;同时讲解对新手不是很友好&#xff0c;故此此文档只讲操作无任何理论讲解。 零、所需文件及环境 1、第四章建立好的串…

51单片机11(蜂鸣器硬件设计和软件设计)

一、蜂鸣器硬件设计 1、 2、上面两张图&#xff0c;是针对不同产品的电路图。像左边这一块&#xff0c;是我们的A2&#xff0c;A3&#xff0c;A4的一个产品对应的一个封闭器的硬件电路。而右边的这一块是对应的A5到A7的一个硬件电路。因为A5到A7的一个产品&#xff0c;它的各…

排序算法3_冒泡排序、快速排序

一、冒泡排序 1.1 冒泡排序定义和思路 冒泡排序的基本思想是&#xff1a;通过相邻两个元素之间的比较和交换&#xff0c;使较大的元素逐渐从前面移向后面&#xff08;升序&#xff09;&#xff0c;就像水底下的气泡一样逐渐向上冒泡&#xff0c;所以被称为“冒泡”排序。  在…

【YOLOv8】 用YOLOv8实现数字式工业仪表智能读数(二)

上一篇圆形表盘指针式仪表的项目受到很多人的关注&#xff0c;咱们一鼓作气&#xff0c;把数字式工业仪表的智能读数也研究一下。本篇主要讲如何用YOLOV8实现数字式工业仪表的自动读数&#xff0c;并将读数结果进行输出&#xff0c;若需要完整数据集和源代码可以私信。 目录 &…

王牌站士Ⅹ---人工智能中的数据隐私:PII 与个人信息

前言 今天&#xff0c;我将讨论如何区分美国和全球范围内不断涌现的数据隐私法所涵盖和不涵盖的数据类型。不同类型的数据受到更严格的保护&#xff0c;具体取决于司法管辖区&#xff0c;因此&#xff0c;如果您使用个人数据进行分析或机器学习&#xff0c;了解这一点很重要。…

痛心!不会用ChatGPT,差点错失一个亿

ChatGPT爆火这么久,今天我们也来聊聊GPT的玩法。等下,什么?你没听说过?没用过? 没听过没用过的朋友们,你们知道当我听到这回答的时候是多么痛心疾首吗? 为了让你们更直观的感受到,举个栗子,如果你用了GPT,就不需要抓耳挠腮的想方案了;如果你用了GPT,或许工作学习效…

MySQL 数据库 - 事务

MySQL 数据库&#xff08;基础&#xff09;- 事务 事务简介 事务 是一组操作集合&#xff0c;他是一个不可分割的工作单位&#xff0c;事务会把所有的操作看作是一个整体一起向系统发送请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 比如&#xff1a;张…

《Python数据科学之三:探索性数据分析与可视化》

《Python数据科学之三&#xff1a;探索性数据分析与可视化》 在数据科学项目中&#xff0c;探索性数据分析&#xff08;EDA&#xff09;和数据可视化是至关重要的步骤。它们帮助数据科学家理解数据的特征、发现数据中的模式和异常值&#xff0c;从而为后续的数据分析和机器学习…

python-29-零基础自学python-json、函数等存取用户数据+验证用户信息

学习内容&#xff1a;《python编程&#xff1a;从入门到实践》第二版 知识点&#xff1a; 如何验证用户、try-except-else处理异常 if判断、def方法及拆解方法 json引入、存储、读取 return none和return变量返回值很重要 answer 1 和answer “1”在使用后的区别 练习内容…

IDEA创建项目模块右边缺少Maven的解决

一、问题描述 我们在创建项目模块时&#xff0c;创建为Maven工程&#xff0c;创建后只是普通工程&#xff0c;idea右边缺少Mavenue标识管理 如图 二、问题的解决方法 在模块的pom.xml文件&#xff0c;点击选项&#xff0c;添加为Maven工程 如图 至此&#xff0c;创建maven工程…