美团一面:说说synchronized的实现原理?问麻了。。。。

news2025/1/6 19:36:03

引言

在现代软件开发领域,多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而,多线程环境下的数据同步与资源共享问题也随之而来,处理不当可能导致数据不一致、死锁等各种并发问题。为此,Java语言提供了一种内置的同步机制——synchronized关键字,它能够有效地解决并发控制的问题,确保共享资源在同一时间只能由一个线程访问,从而维护程序的正确性与一致性。

synchronized作为Java并发编程的基础构建块,其简洁易用的语法形式背后蕴含着复杂的底层实现原理和技术细节。深入理解synchronized的运行机制,不仅有助于我们更好地利用这一特性编写出高效且安全的并发程序,同时也有利于我们在面对复杂并发场景时,做出更为明智的设计决策和优化策略。

本文将从synchronized的基本概念出发,逐步剖析其内在的工作机制,探讨诸如监视器(Monitor)等关键技术点,并结合实际应用场景来展示synchronized的实际效果和最佳实践。通过对synchronized底层实现原理的深度解读,旨在为大家揭示Java并发世界的一隅,提升对并发编程的认知高度和实战能力。

synchronized是什么?

synchronized是Java中实现线程同步的关键字,主要用于保护共享资源的访问,确保在多线程环境中同一时间只有一个线程能够访问特定的代码段或方法。它提供了互斥性和可见性两个重要特性,确保了线程间操作的原子性和数据的一致性。

synchronized的特性

synchronized关键字具有三个基本特性,分别是互斥性、可见性和有序性。

互斥性

synchronized关键字确保了在其控制范围内的代码在同一时间只能被一个线程执行,实现了资源的互斥访问。当一个线程进入了synchronized代码块或方法时,其他试图进入该同步区域的线程必须等待,直至拥有锁的线程执行完毕并释放锁。

可见性

synchronized还确保了线程间的数据可见性。一旦一个线程在synchronized块中修改了共享变量的值,其他随后进入同步区域的线程可以看到这个更改。这是因为synchronized的解锁过程包含了将工作内存中的最新值刷新回主内存的操作,而加锁过程则会强制从主内存中重新加载变量的值。

有序性

synchronized提供的第三个特性是有序性,它可以确保在多线程环境下,对于同一个锁的解锁操作总是先行于随后对同一个锁的加锁操作。这就意味着,通过synchronized建立起了线程之间的内存操作顺序关系,有效地解决了由于编译器和处理器优化可能带来的指令重排序问题。

synchronized可以实现哪锁?

有上述synchronized的特性,我们可以知道synchronized可以实现这些锁:

  1. 可重入锁(Reentrant Lock)synchronized 实现的锁是可重入的,这意味着同一个线程可以多次获取同一个锁,而不会被阻塞。这种锁机制允许线程在持有锁的情况下再次获取相同的锁,避免了死锁的发生。
  2. 排它锁/互斥锁/独占锁synchronized 实现的锁是互斥的,也就是说,在同一时间只有一个线程能够获取到锁,其他线程必须等待该线程释放锁才能继续执行。这确保了同一时刻只有一个线程可以访问被锁定的代码块或方法,从而保证了数据的一致性和完整性。
  3. 悲观锁synchronized 实现的锁属于悲观锁,因为它默认情况下假设会发生竞争,并且会导致其他线程阻塞,直到持有锁的线程释放锁。悲观锁的特点是对并发访问持保守态度,认为会有其他线程来竞争共享资源,因此在访问共享资源之前会先获取锁。
  4. 非公平锁: synchronized在早期的Java版本中,默认实现的是非公平锁,也就是说,线程获取锁的顺序并不一定按照它们请求锁的顺序来进行,而是允许“插队”,即已经在等待队列中的线程可能被后来请求锁的线程抢占。

有关Java中的锁的分类,请参考:阿里二面:Java中锁的分类有哪些?你能说全吗?

synchronized使用方式

synchronized关键字可以修饰方法、代码块或静态方法,用于确保同一时间只有一个线程可以访问被synchronized修饰的代码片段。

修饰实例方法

synchronized修饰实例方法时,锁住的是当前实例对象(this)。这意味着在同一时刻,只能有一个线程访问此方法,所有对该对象实例的其他同步方法调用将会被阻塞,直到该线程释放锁。

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedInstanceMethod sim = new SynchronizedInstanceMethod();
        Thread t1 = new Thread(sim);
        Thread t2 = new Thread(sim);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

像上述这个例子,大家在接触多线程时一定会看过或者写过类似的代码,i++在多线程的情况下是线程不安全的,所以我们使用synchronized作用在累加的方法上,使其变成线程安全的。上述打印结果为:

Final block counter value: 2000

而对于synchronized作用于实例方法上时,锁的是当前实例对象,但是如果我们锁住的是不同的示例对象,那么synchronized就不能保证线程安全了。如下代码:

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedInstanceMethod());
        Thread t2 = new Thread(new SynchronizedInstanceMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

执行结果为:

Final counter value: 1491
修饰静态方法

synchronized修饰的是静态方法,那么锁住的是类的Class对象,因此,无论多少个该类的实例存在,同一时刻也只有一个线程能够访问此静态同步方法。针对修饰实例方法的线程不安全的示例,我们只需要在synchronized修饰的实例方法上加上static,将其变成静态方法,此时synchronized锁住的就是类的class对象。

public class SynchronizedStaticMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private static synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedStaticMethod());
        Thread t2 = new Thread(new SynchronizedStaticMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

执行结果为:

Final counter value: 2000
修饰代码块

通过指定对象作为锁,可以更精确地控制同步范围。这种方式允许在一个方法内部对不同对象进行不同的同步控制。可以指定一个对象作为锁,只有持有该对象锁的线程才能执行被synchronized修饰的代码块。

public class SynchronizedBlock implements Runnable{

    private static int counter = 0;

    @Override
    public void run() {
        // 这个this还可以是SynchronizedBlock.class,说明锁住的是class对象
        synchronized (this){
            for (int i = 0; i < 1000; i++) {
                counter++;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedBlock block = new SynchronizedBlock();
        Thread t1 = new Thread(block);
        Thread t2 = new Thread(block);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

synchronized 内置锁作为一种对象级别的同步机制,其作用在于确保临界资源的互斥访问,实现线程安全。它本质上锁定的是对象的监视器(Object Monitor),而非具体的引用变量。这种锁具有可重入性,即同一个线程在已经持有某对象锁的情况下,仍能再次获取该对象的锁,这显著增强了线程安全代码的编写便利性,并在一定程度上有助于降低因线程交互引起的死锁风险。

关于如何避免死锁,请参考:阿里二面:如何定位&避免死锁?连着两个面试问到了!

synchronized的底层原理

在JDK 1.6之前,synchronized关键字所实现的锁机制确实被认为是重量级锁。这是因为早期版本的Java中,synchronized的实现依赖于操作系统的互斥量(Mutexes)来实现线程间的同步,这涉及到了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和CPU资源的消耗,因此在高并发、低锁竞争的情况下,这种锁机制可能会成为性能瓶颈。

而在JDK 1.6中,对synchronized进行了大量优化,其中包括引入了偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)的概念。接下来我们先说一下JDK1.6之前synchronized的原理。

对象的组成结构

在JDK1.6之前,在Java虚拟机中,Java对象的内存结构主要有对象头(Object Header)实例数据(Instance Data)对齐填充(Padding) 三个部分组成。

  1. 对象头(Object Header)
    对象头主要包含了两部分信息:Mark Word(标记字段)和指向类元数据(Class Metadata)的指针。Mark Word 包含了一些重要的标记信息,比如对象是否被锁定、对象的哈希码、GC相关信息等。类元数据指针指向对象的类元数据,用于确定对象的类型信息、方法信息等。

  2. 实例数据(Instance Data)
    实例数据是对象的成员变量和实例方法所占用的内存空间,它们按照声明的顺序依次存储在对象的实例数据区域中。实例数据包括对象的所有非静态成员变量和非静态方法。

  3. 填充(Padding)
    在JDK 1.6及之前的版本中,为了保证对象在内存中的存储地址是8字节的整数倍,可能会在对象的实例数据之后添加一些填充字节。这些填充字节的目的是对齐内存地址,提高内存访问效率。填充字节通常不包含任何实际数据,只是用于占位。

JDK1.6之前对象结构.png

对象头

在JDK 1.6之前的Java HotSpot虚拟机中,对象头的基本组成依然包含Mark Word和类型指针(Klass Pointer),但当时对于锁的实现还没有引入偏向锁和轻量级锁的概念,因此对象头中的Mark Word在处理锁状态时比较简单,主要是用来存储锁的状态信息以及与垃圾收集相关的数据。在一个32位系统重对象头大小通常约为32位,而在64位系统中大小通常为64位。
对象头组成部分:

  1. Mark Word(标记字)
    在早期版本的HotSpot虚拟机中,Mark Word主要存储的信息包括:
  • 对象的hashCode(在没有锁定时)。
  • 对象的分代年龄(用于垃圾回收算法)。
  • 锁状态信息,如无锁、重量级锁状态(在使用synchronized关键字时)。
  • 对象的锁指针(Monitor地址,当对象被重量级锁锁定时,存储的是指向重量级锁(Monitor)的指针)。

HotSpot虚拟机对象头Mark Word.png

对象头中的Mark Word是一个非固定的数据结构,它会根据对象的状态复用自己的存储空间,存储不同的数据。在Java HotSpot虚拟机中,Mark Word会随着程序运行和对象状态的变化而存储不同的信息。其信息变化如下:

image.png

从存储信息的变化可以看出:

  • 对象头的最后两位存储了锁的标志位,01表示初始状态,即未加锁。此时,对象头内存储的是对象自身的哈希码。无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
  • 当进入偏向锁阶段时,对象头内的标志位变为01,并且存储当前持有锁的线程ID。这意味着只有第一个获取锁的线程才能继续持有锁,其他线程不能竞争同一把锁。
  • 在轻量级锁阶段,标志位变为00,对象头内存储的是指向线程栈中锁记录的指针。这种情况下,多个线程可以通过比较锁记录的地址与对象头内的指针地址来确定自己是否拥有锁。

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的。重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

  1. 类型指针(Klass Pointer 或 Class Pointer)
    类型指针指向对象的类元数据(Class Metadata),即对象属于哪个类的类型信息,用于确定对象的方法表和字段布局等。在一个32位系统重大小通常约为32位,而在64位系统中大小通常为64位。

  2. 数组长度(Array Length)(仅对数组对象适用):
    如果对象是一个数组,对象头中会额外包含一个字段来存储数组的长度。在一个32位系统中大小通常约为32位,而在64位系统中大小通常为64位。

监视器(Monitor)

在Java中,每个对象都与一个Monitor关联,Monitor是一种同步机制,负责管理线程对共享资源的访问权限。当一个Monitor被线程持有时,对象便处于锁定状态。Java的synchronized关键字在JVM层面上通过MonitorEnterMonitorExit指令实现方法同步和代码块同步。MonitorEnter尝试获取对象的Monitor所有权(即获取对象锁),MonitorExit确保每个MonitorEnter操作都有对应的释放操作。

在HotSpot虚拟机中,Monitor具体由ObjectMonitor实现,其结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //锁计数器,表示重入次数,每当线程获取锁时加1,释放时减1。
    _waiters      = 0, //等待线程总数,不一定在实际的ObjectMonitor中有直接体现,但在管理线程同步时是一个重要指标。
    _recursions   = 0; //与_count类似,表示当前持有锁的线程对锁的重入次数。
    _object       = NULL; // 通常指向关联的Java对象,即当前Monitor所保护的对象。
    _owner        = NULL; // 持有ObjectMonitor对象的线程地址,即当前持有锁的线程。
    _WaitSet      = NULL; //存储那些调用过`wait()`方法并等待被唤醒的线程队列。
    _WaitSetLock  = 0 ; // 用于保护_WaitSet的锁。
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //阻塞在EntryList上的单向线程列表,可能用于表示自旋等待队列或轻量级锁的自旋链表。
    FreeNext      = NULL ; // 在对象Monitor池中可能用于链接空闲的ObjectMonitor对象。
    _EntryList    = NULL ; // 等待锁的线程队列,当线程请求锁但发现锁已被持有时,会被放置在此队列中等待。
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; // 标志位,可能用于标识_owner是否指向一个真实的线程对象。
  }

其中最重要的就是_owner_WaitSet_EntryListcount几个字段,他们之间的转换关系:

  1. _owner:
    当一个线程首次成功执行synchronized代码块或方法时,会尝试获取对象的Monitor(即ObjectMonitor),并将自身设置为_owner。该线程此刻拥有了对象的锁,可以独占访问受保护的资源。

  2. _EntryList_owner:
    当多个线程同时尝试获取锁时,除第一个成功获取锁的线程外,其余线程会进入_EntryList排队等待。一旦_owner线程释放锁,_EntryList中的下一个线程将有机会获取锁并成为新的_owner

  3. _owner_WaitSet:
    _owner线程在持有锁的情况下调用wait()方法时,它会释放锁(即_owner置为NULL),并把自己从_owner转变为等待状态,然后将自己添加到_WaitSet中。这时,线程进入等待状态,暂停执行,等待其他线程通过notify()notifyAll()唤醒。

  4. _WaitSet_EntryList:
    当其他线程调用notify()notifyAll()方法时,会选择一个或全部在_WaitSet中的线程,将它们从_WaitSet移除,并重新加入到_EntryList中。这样,这些线程就有机会再次尝试获取锁并成为新的_owner

有上述转换关系我们可以发现,当多线程访问同步代码时:

  1. 线程首先尝试进入_EntryList竞争锁,成功获取Monitor后,将_owner设置为当前线程并将count递增。
  2. 若线程调用wait()方法,会释放Monitor、清空_owner,并将线程移到_WaitSet中等待被唤醒。
  3. 当线程执行完毕或调用notify()/notifyAll()唤醒等待线程后,会释放Monitor,使得其他线程有机会获取锁。

在Java对象的对象头(Mark Word)中,存储了与锁相关的状态信息,这使得任意Java对象都能作为锁来使用,同时,notify/notifyAll/wait等方法正是基于Monitor锁对象来实现的,因此这些方法必须在synchronized代码块中调用。

我们查看上述同步代码块SynchronizedBlock的字节码文件:

image.png

从上述字节码中可以看到同步代码块的实现是由monitorentermonitorexit指令完成的,其中monitorenter指令所在的位置是同步代码块开始的位置,第一个monitorexit指令是用于正常结束同步代码块的指令,第二个monitorexit指令是用于异常结束时所执行的释放Monitor指令。

关于查看class文件的字节码文件,有两种方式:1、通过命令: javap -verbose <class路径>/class文件。2、IDEA中通过插件:jclasslib Bytecode viewer

我们再看一下作用于同步方法的字节码:

image.png

我们可以看出同步方法上没有monitorentermonitorexit 这两个指令了,而在查看该方法的class文件的结构信息时发现了Access flags后边的synchronized标识,该标识表明了该方法是一个同步方法。Java虚拟机通过该标识可以来辨别一个方法是否为同步方法,如果有该标识,线程将持有Monitor,在执行方法,最后释放Monitor。

image.png

总结

synchronized作用于同步代码块时的原理:
Java虚拟机使用monitorenter和monitorexit指令实现同步块的同步。monitorenter指令在进入同步代码块时执行,尝试获取对象的Monitor(即锁),monitorexit指令在退出同步代码块时执行,释放Monitor。

而对于方法级别的同步的原理:
Java虚拟机通过在方法的访问标志(Access flags)中设置ACC_SYNCHRONIZED标志来实现方法同步。当一个方法被声明为synchronized时,编译器会在生成的字节码中插入monitorenter和monitorexit指令,确保在方法执行前后正确地获取和释放对象的Monitor。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。

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

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

相关文章

揭秘ChatGPT的数据集构建

从Open AI发表的论文《Training language models to follow instructions with human feedback》&#xff0c;我们可以知道ChatGPT/InstructGPT&#xff08;两者技术原理基本一致&#xff0c;下面不再做区分&#xff09;的训练过程分为以下3步&#xff1a; SFT&#xff1a;根据…

私域电商客户要挨一刀的“订单发货管理”,微信:必须强制接入

文丨微三云营销总监胡佳东&#xff0c;点击上方“关注”&#xff0c;为你分享市场商业模式电商干货。 - 引言&#xff1a;超90%的私域运营商家都见到了或者说遇到了这个问题&#xff0c;如果没有读懂这个微信的模型机制&#xff0c;一定会懵逼&#xff0c;微三云营销总监胡佳…

C++11:function包装器

包装器&#xff0c;体现了C11中的封装性&#xff0c;包装器可以应用于&#xff1a;函数指针&#xff0c;仿函数&#xff0c;lambda 而包装器function的出现刚好也弥补了上述三种语法的不足之处 函数指针写起来较为复杂&#xff0c;而仿函数之间类型不同&#xff0c;lambda则在…

【Week Y5】yolo.py文件解读,插入C2模块到指定位置

插入C2模块到指定位置 一、common.py文件修改二、yolo.py文件修改三、yolov5s.yaml修改四、训练 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 模块结构如下&#xff1a;【同Y4】 【Y4…

Jackson 2.x 系列【15】序列化器 JsonSerializer

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Jackson 版本 2.17.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-jaskson-demo 文章目录 1. 概述2. 方法2.1 构造2.2 序列化2.3 其他 3. 实现类3.1 StdSerializer3.1.1 源…

CSDN 广告太多,停更通知,转移到博客园

文章目录 前言新博客地址 前言 CSDN的广告实在是太多了&#xff0c;我是真的有点忍不了。直接把广告插在我的文章中间。而且我已经懒得找工作了&#xff0c;我当初写CSDN的目的就是为了找工作&#xff0c;有个博客排名。当时经济环境实在是太差了。我也没必要纠结这个2000粉丝…

Vue通过自定义指令实现元素平滑上升的动画效果。没一句废话

1、演示 2、介绍 这个指令不是原生自带的&#xff0c;需要手动去书写&#xff0c;但是这辈子只需要编写这一次就好了&#xff0c;后边可以反复利用。 用到的API&#xff1a;IntersectionObserver 这里有详细介绍 3、Vue文件代码 <template><div class"container&…

【计算机考研】408算法大题怎么练?

先说结论&#xff1a;基础阶段学好各个数据结构与&#xff0c;重点是数组、链表、树、图。然后强化阶段突破算法提 在基础阶段&#xff0c;并不需要过于专门地练习算法。相反&#xff0c;基础阶段的重点应该放在对各种数据结构原理的深入理解上。在我个人的经验中&#xff0c;…

【AcWing】蓝桥杯集训每日一题Day17|单调队列|求直方图中最大矩形|单调栈|模型转化|1413.矩形牛棚(C++)

1413.矩形牛棚 1413. 矩形牛棚 - AcWing题库难度&#xff1a;中等时/空限制&#xff1a;1s / 256MB总通过数&#xff1a;1914总尝试数&#xff1a;3823来源&#xff1a;usaco training 6.1算法标签单调栈 题目内容 作为一个资本家&#xff0c;农夫约翰希望通过购买更多的奶牛…

【零基础学数据结构】链表

目录 1.链表的概念 ​编辑 2.链表的雏形 ​编辑 3.链表的组成 ​编辑 4.链表代码 4.1创建节点 4.2链表的打印 4.3链表的尾插 4.4链表的头插 4.5链表的尾删 4.6链表的头删 4.7链表的查找 4.8链表在指定位置之前插⼊数据 4.9链表在指定位置之后插⼊数据 4.9-1删除pos节点 4.9…

VBA技术资料MF140:在PowerPoint中移动幻灯片位置

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

Java基于微信小程序的校园外卖平台设计与实现,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

openGauss学习笔记-255 openGauss性能调优-使用Plan Hint进行调优-Hint的错误、冲突及告警

文章目录 openGauss学习笔记-255 openGauss性能调优-使用Plan Hint进行调优-Hint的错误、冲突及告警 openGauss学习笔记-255 openGauss性能调优-使用Plan Hint进行调优-Hint的错误、冲突及告警 Plan Hint的结果会体现在计划的变化上&#xff0c;可以通过explain来查看变化。 …

公开课学习——JVM虚拟机面试核心点与性能优化点

文章目录 jdk的体系结构图Java语言的跨平台的特性&#xff0c;怎么实现的&#xff1f;jvm内部组成呢&#xff1f;pc的值怎么变得&#xff1f;main方法的栈帧有一点点区别&#xff0c;Math()是new出来的&#xff0c;放在堆区&#xff0c;这个堆区的math和我们栈帧中的局部变量表…

SpringBoot之集成Redis

SpringBoot之集成Redis 一、Redis集成简介二、集成步骤2.1 添加依赖2.2 添加配置2.3 项目中使用 三、工具类封装四、序列化 &#xff08;正常都需要自定义序列化&#xff09;五、分布式锁 一、Redis集成简介 Redis是我们Java开发中&#xff0c;使用频次非常高的一个nosql数据库…

【频繁模式挖掘】Apriori算法(附Python实现)

一、实验内容简介 该实验主要使用频繁模式和关联规则进行数据挖掘&#xff0c;使用Apriori算法和Python语言来编写和设计程序&#xff0c;然后用不同规模的数据集来检验效果&#xff0c;最后分析和探讨实验结果&#xff0c;看其是否达到了理想的效果。 二、算法说明 关联规则…

2024年生成式人工智能的现状:进展、挑战与未来展望的深入分析

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

wireshark抓包新手使用教程

Wireshark是非常流行的网络封包分析软件&#xff0c;可以截取各种网络数据包&#xff0c;并显示数据包详细信息。常用于开发测试过程各种问题定位。本文主要内容包括&#xff1a; 1、Wireshark软件下载和安装以及Wireshark主界面介绍。 2、WireShark简单抓包示例。通过该例子学…

【C++航海王:追寻罗杰的编程之路】探寻实用的调试技巧

目录 1 -> 什么是bug&#xff1f; 2 -> 调试是什么&#xff1f;有多重要&#xff1f; 2.1 -> 调试是什么&#xff1f; 2.2 -> 调试的基本步骤 2.3 -> Debug和Release的介绍 3 -> Windows环境调试介绍 3.1 -> 调试环境的准备 3.2 -> 学会快捷键…

不牺牲算法,不挑剔芯片,这个来自中科院的团队正在加速国产AI芯片破局

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 不降低大模型算法精度&#xff0c;还能把芯片的算力利用效率提升 2~10 倍&#xff0c;这就是…