.Net 线程安全 (细粒度锁定、无锁机制以及lock)

news2024/11/18 15:50:39

.Net 线程安全

  • 最省事的lock关键字
  • 线程安全对象
  • 测试环境
  • 例子
    • 使用Queue源码和结果
      • 运行效果
    • 使用ConcurrentQueue的源码和结果
      • 运行效果
  • volatile关键字
    • 易失性内存和非易失性内存的区别
      • 易失性内存:
      • 非易失性内存:
    • volatile 关键字可应用于以下类型的字段:
    • 测试代码(添加volatile 关键字)
    • 测试效果(添加volatile 关键字)
    • 测试代码(==没有volatile 关键字==)
    • 测试效果(==没有volatile 关键字==)
  • Interlocked
    • Decrement和Increment方法递增或递减变量,并将结果值存储在单个操作中。 在大多数计算机上,递增变量不是原子操作,需要以下步骤:
  • 细粒度锁定和无锁机制 (SpinWait、 SpinLock)
    • 看一下SpinWait的SpinOnce源代码
    • SpinLock

最省事的lock关键字

https://blog.csdn.net/iml6yu/article/details/74984466

在多线程操作过程中,最省事的安全操作关键字就是lock,但是这会影响到多线程操作的性能。

线程安全对象

.NET Framework 4 引入了 System.Collections.Concurrent 命名空间,其中包含多个线程安全且可缩放的集合类。 多个线程可以安全高效地从这些集合添加或删除项,而无需在用户代码中进行其他同步。 编写新代码时,只要将多个线程同时写入到集合时,就使用并发集合类。

在这里插入图片描述
简单来说使用上述的对象进行多线程之间操作的时候都能确保线程安全

测试环境

  • win10
  • vs2022
  • .net6
  • 语言版本 c#10

例子

例子中使用一个主线程往队列中写入一些数据,然后分10个线程进行读取,
分别使用QueueConcurrentQueue进行测试,对比两次结果

使用Queue源码和结果

using System.Collections.Concurrent;
using System.Diagnostics;

namespace 一写多读_NetCore_v6
{
    internal class Program
    {
        //声明读取结果接收容器列表
        static List<List<string>> list
                = new List<List<string>>() {
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>()
                };
        //队列用于存储生产数据的(一个写入,多个读取)
        static Queue<string> strings = new Queue<string>();
        //避免程序不能结束声明的变量,表示当前写入线程是否已经把所有数据都写完了
        static bool IsReadyComplated = false;
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
          
            //开10个线程去读取数据
            Task.Run(() =>
            {
                var result = Parallel.ForEach(list, ls =>
                  {
                      while (!IsReadyComplated || strings.Count > 0)
                      {
                          if (strings.Count > 0)
                          {
                              string s;
                              strings.TryDequeue(out s);
                              if (s != null)
                                  ls.Add(s);
                          }

                          //Task.Delay(0).Wait();
                      }

                  });
                //判断每个线程读取到的结果是否有重复,也就是是否出现了脏读的问题
                if (result.IsCompleted)
                {
                    Console.WriteLine($"读取到的总数据条数:{list.Sum(ls=>ls.Count)}");
                    list.ForEach(ls =>
                    {
                        if (ls.Count != ls.Distinct().Count())
                        {
                            Console.WriteLine("存在重复数据");
                        }
                        foreach (var l in list)
                        {
                            if (l != ls)
                            {
                                if (l.Where(item => ls.Contains(item)).Count() > 0)
                                    Console.WriteLine("存在重复数据 Item");
                            }
                        }
                    });
                    Console.WriteLine("执行完成!");
                }
                
            });
            //生产数据
            OneWrite();//.Wait();

            //确保在判断数据的时候程序没有退出而写的一个死循环
            while (true)
            {

                Task.Delay(TimeSpan.FromMinutes(10)).Wait();
            }
        }

        private static void OneWrite()
        {
            IsReadyComplated = false;
            Stopwatch stopwatch = new Stopwatch();
            //总数居条数
            int total = 100000;


            stopwatch.Start();
            while (total > 0)
            {
                strings.Enqueue(total.ToString());
                //Task.Delay(1).Wait();//158630.1541
                //Thread.Sleep(1);//155031.3485
                //await Task.Delay(1);//158206.7742
                total--;
            }
            IsReadyComplated = true;
            stopwatch.Stop();
            Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds.ToString());
        }
    }
}

运行效果

我代码中实际生产了100000条数据,但是读取后却是118639条,所以一定是有线程读取出现了重复读取的问题
在这里插入图片描述

使用ConcurrentQueue的源码和结果

static ConcurrentQueue strings = new ConcurrentQueue();

using System.Collections.Concurrent;
using System.Diagnostics;

namespace 一写多读_NetCore_v6
{
    internal class Program
    {
        //声明读取结果接收容器列表
        static List<List<string>> list
                = new List<List<string>>() {
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>(),
                    new List<string>()
                };
        //队列用于存储生产数据的(一个写入,多个读取)
        static ConcurrentQueue<string> strings = new ConcurrentQueue<string>();
        //避免程序不能结束声明的变量,表示当前写入线程是否已经把所有数据都写完了
        static bool IsReadyComplated = false;
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
          
            //开10个线程去读取数据
            Task.Run(() =>
            {
                var result = Parallel.ForEach(list, ls =>
                  {
                      while (!IsReadyComplated || strings.Count > 0)
                      {
                          if (strings.Count > 0)
                          {
                              string s;
                              strings.TryDequeue(out s);
                              if (s != null)
                                  ls.Add(s);
                          }

                          //Task.Delay(0).Wait();
                      }

                  });
                //判断每个线程读取到的结果是否有重复,也就是是否出现了脏读的问题
                if (result.IsCompleted)
                {
                    Console.WriteLine($"读取到的总数据条数:{list.Sum(ls=>ls.Count)}");
                    list.ForEach(ls =>
                    {
                        if (ls.Count != ls.Distinct().Count())
                        {
                            Console.WriteLine("存在重复数据");
                        }
                        foreach (var l in list)
                        {
                            if (l != ls)
                            {
                                if (l.Where(item => ls.Contains(item)).Count() > 0)
                                    Console.WriteLine("存在重复数据 Item");
                            }
                        }
                    });
                    Console.WriteLine("执行完成!");
                }
                
            });
            //生产数据
            OneWrite();//.Wait();

            //确保在判断数据的时候程序没有退出而写的一个死循环
            while (true)
            {

                Task.Delay(TimeSpan.FromMinutes(10)).Wait();
            }
        }

        private static void OneWrite()
        {
            IsReadyComplated = false;
            Stopwatch stopwatch = new Stopwatch();
            int total = 100000;


            stopwatch.Start();
            while (total > 0)
            {
                strings.Enqueue(total.ToString());
                //Task.Delay(1).Wait();//158630.1541
                //Thread.Sleep(1);//155031.3485
                //await Task.Delay(1);//158206.7742
                total--;
            }
            IsReadyComplated = true;
            stopwatch.Stop();
            Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds.ToString());
        }
    }
}

运行效果

在这里插入图片描述

volatile关键字

volatile 关键字指示一个字段可以由多个同时执行的线程修改。 出于性能原因,编译器,运行时系统甚至硬件都可能重新排列对存储器位置的读取和写入。 声明为 volatile 的字段将从某些类型的优化中排除。

多个线程同时访问一个变量,CLR为了效率,允许每个线程进行本地缓存,这就导致了变量的不一致性。volatile就是为了解决这个问题,volatile修饰的变量,不允许线程进行本地缓存,每个线程的读写都是直接操作在共享内存上,这就保证了变量始终具有一致性。

在多处理器系统上,由于编译器或处理器中的性能优化,当多个处理器在同一内存上运行时,常规内存操作似乎被重新排序。 易失性内存操作阻止对操作进行某些类型的重新排序。 易失性写入操作可防止对线程的早期内存操作重新排序,以在易失性写入之后发生。 易失性读取操作可防止对线程的后续内存操作重新排序,以在易失读取之前发生。 这些操作可能涉及某些处理器上的内存屏障,这可能会影响性能

易失性内存和非易失性内存的区别

易失性内存:

它是高速获取/存储数据的内存硬件。它也被称为临时内存。易失性内存中的数据会一直保存到系统可以使用,但是一旦系统关闭,易失性内存中的数据就会被自动删除。RAM(随机存取存储器)和高速缓存存储器是易失性存储器的一些常见示例。在这里,数据获取/存储既快速又经济。

非易失性内存:

这是一种内存类型,即使断电,数据或信息也不会在内存中丢失。ROM(只读存储器)是非易失性存储器的最常见示例。与易失性存储器相比,它在获取/存储方面并不经济且速度慢,但存储的数据量更大。所有需要长时间存储的信息都存储在非易失性存储器中。非易失性存储器对系统的存储容量有巨大影响。

volatile 关键字可应用于以下类型的字段:

  • 引用类型。
  • 指针类型(在不安全的上下文中)。 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。 换句话说,不能声明“指向可变对象的指针”。
  • 简单类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
  • 具有以下基本类型之一的 enum 类型:byte、sbyte、short、ushort、int 或 uint。
  • 已知为引用类型的泛型类型参数。
  • IntPtr 和 UIntPtr。
  • 其他类型(包括 double 和 long)无法标记为 volatile,因为对这些类型的字段的读取和写入不能保证是原子的

测试代码(添加volatile 关键字)

namespace volatile关键字测试
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Test test = new Test();
            Console.WriteLine("Hello, World!");
            Thread thread1 = new Thread(new ThreadStart(() =>
            { 
                test.x = 1;
                test.y = 1;
                Console.WriteLine($"x:{test.x}    y:{test.y}");
            }));

            Thread thread2 = new Thread(new ThreadStart(() =>
            {
                int y2 = test.y;
                int x2 = test.x;
                Console.WriteLine($"x2:{x2}    y2:{y2}");
            }));

             thread1.Start();  
             thread2.Start(); 
         
        }
    }

    public class Test
    {
        public volatile int x;
        public volatile int y;
    }
}

测试效果(添加volatile 关键字)

在这里插入图片描述

测试代码(没有volatile 关键字

namespace volatile关键字测试
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Test test = new Test();
            Console.WriteLine("Hello, World!");
            Thread thread1 = new Thread(new ThreadStart(() =>
            { 
                test.x = 1;
                test.y = 1;
                Console.WriteLine($"x:{test.x}    y:{test.y}");
            }));

            Thread thread2 = new Thread(new ThreadStart(() =>
            {
                int y2 = test.y;
                int x2 = test.x;
                Console.WriteLine($"x2:{x2}    y2:{y2}");
            }));

             thread1.Start();  
             thread2.Start(); 
         
        }
    }

    public class Test
    {
        public   int x;
        public  int y;
    }
}

测试效果(没有volatile 关键字

在这里插入图片描述

Interlocked

为多个线程共享的变量提供原子操作。

此类的方法有助于防止当计划程序切换上下文时,当线程更新可由其他线程访问的变量时,或者当两个线程同时在单独的处理器上执行时,可能会出现的错误。 此类的成员不会引发异常。

Decrement和Increment方法递增或递减变量,并将结果值存储在单个操作中。 在大多数计算机上,递增变量不是原子操作,需要以下步骤:

  1. 将实例变量的值加载到寄存器中。

  2. 递增或递减值。

  3. 将值存储在实例变量中。

如果不使用 Increment 并且 Decrement执行前两个步骤后,可以抢占线程。 然后,另一个线程可以执行所有三个步骤。 当第一个线程恢复执行时,它会覆盖实例变量中的值,并丢失第二个线程执行的递增或递减的效果。

该方法 Add 以原子方式将整数值添加到整数变量,并返回该变量的新值。

该方法 Exchange 以原子方式交换指定变量的值。 该方法 CompareExchange 结合两个操作:根据比较的结果,比较两个值,并将第三个值存储在其中一个变量中。 比较和交换操作作为原子操作执行。

确保对共享变量的任何写入或读取访问权限都是原子的。 否则,数据可能会损坏,或者加载的值可能不正确。

https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=net-7.0

细粒度锁定和无锁机制 (SpinWait、 SpinLock)

https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.spinwait?source=recommendations&view=net-7.0

在查看ConcurrentQueue<T>源码时,发现了这个类
这段代码是 ConcurrentQueue<T>的Enqueue方法

/// <summary>
        /// Adds an object to the end of the <see cref="ConcurrentQueue{T}"/>.
        /// </summary>
        /// <param name="item">The object to add to the end of the <see
        /// cref="ConcurrentQueue{T}"/>. The value can be a null reference
        /// (Nothing in Visual Basic) for reference types.
        /// </param>
        public void Enqueue(T item)
        {
            SpinWait spin = new SpinWait();
            while (true)
            {
                Segment tail = m_tail;
                if (tail.TryAppend(item))
                    return;
                spin.SpinOnce();
            }
        }

System.Threading.SpinWait 是一种轻型同步类型,可用于低级方案,以避免执行内核事件所需的高成本上下文切换和内核转换。 在多核计算机上,如果不得长时间保留资源,更高效的做法是,先让等待线程在用户模式下旋转几十或几百个周期,再重试获取资源。 如果资源在旋转后可用,便节省了几千个周期。 如果资源仍不可用,那么也只花了几个周期,仍可以进入基于内核的等待。 这种“旋转后等待”的组合有时称为“两阶段等待操作” 。

看一下SpinWait的SpinOnce源代码

/// <summary>
        /// Performs a single spin.
        /// </summary>
        /// <remarks>
        /// This is typically called in a loop, and may change in behavior based on the number of times a
        /// <see cref="SpinOnce"/> has been called thus far on this instance.
        /// </remarks>
        public void SpinOnce()
        {
            if (NextSpinWillYield)
            {
                //
                // We must yield.
                //
                // We prefer to call Thread.Yield first, triggering a SwitchToThread. This
                // unfortunately doesn't consider all runnable threads on all OS SKUs. In
                // some cases, it may only consult the runnable threads whose ideal processor
                // is the one currently executing code. Thus we oc----ionally issue a call to
                // Sleep(0), which considers all runnable threads at equal priority. Even this
                // is insufficient since we may be spin waiting for lower priority threads to
                // execute; we therefore must call Sleep(1) once in a while too, which considers
                // all runnable threads, regardless of ideal processor and priority, but may
                // remove the thread from the scheduler's queue for 10+ms, if the system is
                // configured to use the (default) coarse-grained system timer.
                //
 
#if !FEATURE_PAL && !FEATURE_CORECLR   // PAL doesn't support  eventing, and we don't compile CDS providers for Coreclr
                CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
#endif
                int yieldsSoFar = (m_count >= YIELD_THRESHOLD ? m_count - YIELD_THRESHOLD : m_count);
 
                if ((yieldsSoFar % SLEEP_1_EVERY_HOW_MANY_TIMES) == (SLEEP_1_EVERY_HOW_MANY_TIMES - 1))
                {
                    Thread.Sleep(1);
                }
                else if ((yieldsSoFar % SLEEP_0_EVERY_HOW_MANY_TIMES) == (SLEEP_0_EVERY_HOW_MANY_TIMES - 1))
                {
                    Thread.Sleep(0);
                }
                else
                {
#if PFX_LEGACY_3_5
                    Platform.Yield();
#else
                    Thread.Yield();
#endif
                }
            }
            else
            {
                //
                // Otherwise, we will spin.
                //
                // We do this using the CLR's SpinWait API, which is just a busy loop that
                // issues YIELD/PAUSE instructions to ensure multi-threaded CPUs can react
                // intelligently to avoid starving. (These are NOOPs on other CPUs.) We
                // choose a number for the loop iteration count such that each successive
                // call spins for longer, to reduce cache contention.  We cap the total
                // number of spins we are willing to tolerate to reduce delay to the caller,
                // since we expect most callers will eventually block anyway.
                //
                Thread.SpinWait(4 << m_count);
            }
 
            // Finally, increment our spin counter.
            m_count = (m_count == int.MaxValue ? YIELD_THRESHOLD : m_count + 1);
        } 

这个方法里面涉及到一些平台的东西,但是可以看到内部还是有一些Thread.Sleep的方法和Thread.Yield方法。

SpinLock

https://learn.microsoft.com/zh-cn/dotnet/standard/threading/how-to-use-spinlock-for-low-level-synchronization

关键部分执行的工作量最少,因而非常适合执行 SpinLock。 与标准锁相比,增加一点工作量即可提升 SpinLock 的性能。 但是,超过某个点时 SpinLock 将比标准锁开销更大

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

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

相关文章

<C++>AVL数

文章目录1. AVL树的概念2. AVL树节点的定义3. AVL树的插入4. AVL树的旋转5. AVL树的验证6. AVL树的性能1. AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0…

MySQL中alter命令知识

MySQL中alter命令知识 文章目录MySQL中alter命令知识(一) 删除、添加、修改字段1、删除表中的字段数据2、添加新字段指定字段插入位置3、修改字段类型和名称&#xff08;二&#xff09;、修改表名&#xff08;三&#xff09;、修改存储引擎创建copy_emp表&#xff0c;便于后面案…

Prompt Learning 简介

最近去参会&#xff0c;看到了大量关于Prompt相关的论文&#xff0c;或者说跟NLP NLU相关的新论文或多或少都使用到了Prompt learning的一些思想或者设置。由于本人主业不是是做NLP的&#xff0c;所以对NLP顶会的这一现象觉得很有意思&#xff0c;趁闲暇学习了一下Prompt learn…

对话 BitSail Contributor | 姚泽宇:新生火焰,未来亦可燎原

2022 年 10 月&#xff0c;字节跳动 BitSail 数据引擎正式开源。同期&#xff0c;社区推出 Contributor 激励计划第一期&#xff0c;目前已有 12 位开发者为 BitSail 社区做出贡献&#xff0c;成为了首批 BitSail Contributor。 江海的广阔是由每一滴水珠构成的&#xff0c;Bi…

【高阶数据结构】手撕哈希表(万字详解)

&#x1f308;欢迎来到数据结构专栏~~手撕哈希表 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句…

【jqgrid篇】jqgrid.setCell 改变单元格的值 改变单元格的样式设置单元格属性

setCellrowid,colname, data, class, propertiesjqGrid对象 改变单元格的值。rowid&#xff1a;当前行id&#xff1b;colname&#xff1a;列名称&#xff0c;也可以是列的位置索引&#xff0c;从0开始&#xff1b;data&#xff1a;改变单元格的内容&#xff0c;如果为空则不更 …

将GO、Pathway富集结果整合在一张高颜值圆圈图上

富集分析是生物医学论文中非常常见的一类分析&#xff0c;例如GO富集分析&#xff0c;Pathway富集分析等。其结果一般包括以下几个要素&#xff1a;1&#xff0c;名字&#xff08;GO term或者KEGG description&#xff09;&#xff1b;2&#xff0c;该名字所包含的基因数目&…

400G数据中心短距离传输方案:400G QSFP-DD SR8光模块

随着更快、更高可靠性的网络需求增加&#xff0c;400G将是下一代骨干网升级和新建设的方向。400G光模块在构建400G网络系统中起着至关重要的作用。前面我们为大家介绍了短距离单模应用的400G QSFP-DD DR4光模块&#xff0c;本期文章&#xff0c;我们一起来了解一下短距离多模光…

自定义类型:结构体,枚举,联合(2)

TIPS 1. 类型的定义可以考虑放在头文件里头。 2. 一个汉字存储的时候占两个字节空间 3. 关于结构体变量初始化的一些细节 4. 关于结构体内存对齐的补充 1. 2. S1和S2类型的成员一模一样&#xff0c;但是S1和S2所占空间的大小有了一些区别。 3. 这两个结构体类型成员都…

【Linux】六、Linux 基础IO(一)|重谈文件|C语言文件操作|操作系统文件操作(系统文件I/O)|文件描述符

目录 一、重谈文件 二、C语言文件操作 2.1 重谈C语言文件操作 2.2 补充细节 三、操作系统文件操作&#xff08;系统文件I/O&#xff09; 3.1 文件相关系统调用&#xff1a;close 3.2 文件相关系统调用&#xff1a;open 3.2.1 open 的第二个参数 flags 3.2.2 open 的第…

解决跨微服务调用token共享问题

场景描述 使用jeecg搭建SpringCloud微服务系统模块&#xff0c;各个系统模块单独创建了拦截器进行权限校验。结果发现跨微服务调用存在鉴权失败问题。不能正常跨微服务调用。 原因描述 单个微服务鉴权拦截器。 package org.jeecg.modules.taxation.inerceptor;import org.s…

【MySQL】MySQL单表操作

序号系列文章2【MySQL】MySQL基本操作详解3【MySQL】MySQL基本数据类型4【MySQL】MySQL表的七大约束5【MySQL】字符集与校对集详解文章目录MySQL单表操作1&#xff0c;数据操作1.1&#xff0c;复制表结构和数据1.2&#xff0c;解决主键冲突1.3&#xff0c;清空数据1.4&#xff…

二叉树详解(概念+遍历实现)

一、基本概念 1.最左孩子结点&#xff1a;一个结点的孩子结点中位于最左边的孩子结点。例如&#xff0c;A——B&#xff0c;B——E&#xff1b; 2.树的高度&#xff1a;树的最高层数&#xff1b; 3.路径长度&#xff1a;树中的任意两个顶点之间都存在唯一的一条路径。一条路径所…

我们这样做容器分层性能测试

前言目前闲鱼不少业务正在从H5/Weex升级到Kun&#xff08;基于W3C标准&Flutter打造的混合高性能终端容器&#xff09;&#xff0c;从测试角度来看&#xff0c;我们希望这种升级迭代对于用户体验是正向的&#xff0c;所以用好性能测试这把标准尺就显得格外重要。早期做性能保…

有什么比较好用的低代码开发平台?

国内有特色的低代码快速开发平台产品有哪些&#xff1f;这篇就来介绍下目前市面上主要的几家零代码开发平台&#xff01; 简道云、明道云、IVX这几家目前是无代码赛道的明星选手&#xff0c;在市场综合表现上名列前茅。宜创、红圈营销虽也极具潜力&#xff0c;但在市场表现力上…

Java开发技术之成为高级java工程师必须学习的三个技术

所谓的Java高级程序员往往是经验和能力的结合&#xff0c;并不是说掌握了哪几个技术就是高级程序员了&#xff0c;能否把掌握的知识运用到实际的项目中&#xff0c;并且解决了具体的问题&#xff0c;这个才是衡量一个Java程序员的标准。 那么对于一名Java程序员来说&#xff0…

Java项目:房屋租赁系统设计和实现(java+ssm+mysql+spring+jsp)

源码获取&#xff1a;博客首页 "资源" 里下载&#xff01; 主要功能描述&#xff1a; 1.登录管理&#xff1a;主要有管理员登录和租客登录 2.房源列表以及添加房源功能&#xff1a; 3.租赁合同管理以及在租房源和已退租房源信息管理: 4.看房申请和退租申请管理&a…

【 java 集合】HashMap源码分析

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

python基础篇之列表(增删改查)

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

excel数据统计:三个公式提高统计工作效率

善于在工作中使用函数、公式可以提高工作效率&#xff0c;结合近期学员们遇到的问题&#xff0c;老菜鸟总结了三个非常实用的公式&#xff0c;每个公式都可以解决一类问题。学会这三个公式套路&#xff0c;就能解决日常遇到的很多麻烦事。第一类问题&#xff1a;对指定时间段的…