再说多线程(二)——细说Monitor类

news2025/1/16 18:06:55

在上一节我们已经讨论了使用Lock来保证并发程序的一致性,Lock锁是借助了Monitor类的功能。本节将详细的介绍Monitor类,以及如何通过Monitor类的成员函数实现并行程序的一致性。


1.Monitor类介绍

根据微软的说法,C#中的监视器类提供了一种同步对象访问的机制。让我们简化上面的定义。简而言之,我们可以说,像锁一样,我们也可以使用这个监视器类来保护多线程环境中的共享资源免受并发访问。这可以通过获取对象上的独占锁来完成,以便在任何给定时间点只有一个线程可以进入关键部分。

Monitor 类是一个静态类,属于 System.Threading 命名空间。作为一个静态类,它提供了一个静态方法的集合,如下图所示。使用这些静态方法,我们可以提供对与特定对象相关联的关键部分的同步访问。

解释一下Monitor类的成员函数:

  1. Enter():当我们调用 Monitor 类的 Enter 方法时,它会在指定对象上获取一个独占锁。这也标志着关键部分的开始或共享资源的开始。

  1. Exit():当调用 Monitor 类的 Exit 方法时,它会释放指定对象上的锁。这也标志着关键部分的结束或受锁定对象保护的共享资源的结束。

  1. Pules():当调用 Monitor 类的 Pulse 方法时,它会向等待队列中的线程发送锁定对象状态更改的信号。

  1. Wait():当调用 Monitor 类的 Wait 方法时,它会释放对象上的锁并阻止当前线程,直到它重新获取锁。

  1. PulesAll():当从监视器类调用 PulseAll 方法时,它会将信号发送到锁定对象状态更改的所有等待线程。

  1. TryEnter():当我们调用 Monitor 类的 TryEnter 方法时,它会尝试获取指定对象上的独占锁。

2.Enter和Exit的基本用法

Enter和Exit的基本用法如下:

通俗一点来说,当某个线程使用Enter函数后,在执行到Exit前,这之间的代码都被会被锁住,Exit执行后,其它线程可以重新调用Enter进入,也就是Monitor类中有一把钥匙,谁调用Enter时,钥匙就分给这个进程,执行完推出时,钥匙重新回到Monitor类,然后钥匙可以分给其它线程。

现在看一个例子:

 internal class MonitorBasicUasage
    {
        public static readonly object locker=new object();
        public static void Test()
        {
            Thread[] threads=new Thread[3];
            for(int i=0; i<threads.Length; i++)
            {
                threads[i] = new Thread(Print)
                {
                    Name = "Child Thread " + i
                };
            }
            Array.ForEach(threads,t=>t.Start());    
            Console.ReadLine();
        }
        public static void Print()
        {
            Console.WriteLine(Thread.CurrentThread.Name + " Trying to enter into the critical section");
            try
            {
                Monitor.Enter(locker);
                Console.WriteLine(Thread.CurrentThread.Name + " Entered into the critical section");
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(100);
                    Console.Write(i + ",");
                }
                Console.WriteLine();
            }
            finally
            {
                Monitor.Exit(locker);
                Console.WriteLine(Thread.CurrentThread.Name + " Exit from critical section");
            }
        }
    }

代码并不难,运行结果如下:

Child Thread 0 Trying to enter into the critical section
Child Thread 1 Trying to enter into the critical section
Child Thread 2 Trying to enter into the critical section
Child Thread 0 Entered into the critical section
0,1,2,3,4,
Child Thread 0 Exit from critical section
Child Thread 1 Entered into the critical section
0,1,2,3,4,
Child Thread 1 Exit from critical section
Child Thread 2 Entered into the critical section
0,1,2,3,4,
Child Thread 2 Exit from critical section

分析上面的结果可以看出,执行第一个Console.WriteLine时,程序时并行执行的,三个线程的输出都先于for循环的执行,但是再执行for循环时,即使有Sleep函数,也不会打乱for循环的执行顺序,必然时一个再释放锁之后,才轮得到另一个线程执行。

3.Monitor运行机制

C# 中的 Monitor 类提供了一种基于等待的同步机制,该机制一次只允许一个线程访问关键部分代码,以避免争用条件。所有其他线程必须等待并停止执行,直到释放锁定的对象。

若要了解 Monitor 类在 C# 中的工作方式,请查看下图。如下图所示,只要线程执行 Thread 类的 Enter 方法,它就会在就绪队列中,并且以同样的方式,许多线程可以存在于就绪队列中。然后,就绪队列中的一个线程将在对象上获取独占锁,并将进入关键部分并执行代码,此时,没有其他线程有机会进入关键部分。然后,当我们执行 Thread 类的 Exit 方法时,当前正在执行的线程将进入等待队列,并向 Ready 队列中的线程发送一个信号,并且 Ready 队列中的一个线程将获取锁并将进入关键部分并开始执行关键部分的代码。这就是监视器类在 C# 中的工作方式。

4.Enter重载(一)

这里顺便说一下Enter方法的另一个重载,Monitor.Enter(lockObject, ref IslockTaken) 获取指定对象的独占锁。然后,它会自动设置一个值,指示是否已锁定。第二个参数是一个布尔参数,如果获取了锁,则返回 true,否则返回 false。下面给出了使用此重载版本的语法。

我们根据上面的逻辑重新修改之前的程序:

 internal class MonitorBasicUasage
    {
        public static readonly object locker=new object();
        public static void Test()
        {
            Thread[] threads=new Thread[3];
            for(int i=0; i<threads.Length; i++)
            {
                threads[i] = new Thread(Print)
                {
                    Name = "Child Thread " + i
                };
            }
            Array.ForEach(threads,t=>t.Start());    
            Console.ReadLine();
        }
        public static void Print()
        {
            Console.WriteLine(Thread.CurrentThread.Name + " Trying to enter into the critical section");
            bool isLockTaken = false;
            try
            {
                Monitor.Enter(locker,ref isLockTaken);
                if(isLockTaken)
                    Console.WriteLine(Thread.CurrentThread.Name + " Entered into the critical section");
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(100);
                    Console.Write(i + ",");
                }
                Console.WriteLine();
            }
            finally
            {
                if(isLockTaken)
                {
                    Monitor.Exit(locker);
                    Console.WriteLine(Thread.CurrentThread.Name + " Exit from critical section");
                }
            }
        }

运行结果如下:

Child Thread 1 Trying to enter into the critical section
Child Thread 1 Entered into the critical section
Child Thread 0 Trying to enter into the critical section
Child Thread 2 Trying to enter into the critical section
0,1,2,3,4,
Child Thread 1 Exit from critical section
Child Thread 0 Entered into the critical section
0,1,2,3,4,
Child Thread 0 Exit from critical section
Child Thread 2 Entered into the critical section
0,1,2,3,4,
Child Thread 2 Exit from critical section

结果略有变化,不再时线程0最先进入,但是最先进入的一定会最先推出,且再for循环中时独立的。

在运行一下又会得到另一个结果:

Child Thread 0 Trying to enter into the critical section
Child Thread 1 Trying to enter into the critical section
Child Thread 2 Trying to enter into the critical section
Child Thread 0 Entered into the critical section
0,1,2,3,4,
Child Thread 1 Entered into the critical section
Child Thread 0 Exit from critical section
0,1,2,3,4,
Child Thread 2 Entered into the critical section
Child Thread 1 Exit from critical section
0,1,2,3,4,
Child Thread 2 Exit from critical section

5. Enter重载二

TryEnter(Object, TimeSpan, Boolean) 应该很好理解,在原先的基础上加了一个等待时间,如果在这个时间内没有获取到锁,那么便不会执行critical section区域。

我们再次尝试一下:

 internal class MonitorBasicUasage
    {
        public static readonly object locker=new object();
        public static void Test()
        {
            Thread[] threads=new Thread[3];
            for(int i=0; i<threads.Length; i++)
            {
                threads[i] = new Thread(Print)
                {
                    Name = "Child Thread " + i
                };
            }
            Array.ForEach(threads,t=>t.Start());    
            Console.ReadLine();
        }
        public static void Print()
        {
            Console.WriteLine(Thread.CurrentThread.Name + " Trying to enter into the critical section");
            bool isLockTaken = false;
            try
            {
                Monitor.TryEnter(locker,TimeSpan.FromMilliseconds(1000),ref isLockTaken);
                if(isLockTaken)
                {
                    Console.WriteLine(Thread.CurrentThread.Name + " Entered into the critical section");
                    for (int i = 0; i < 5; i++)
                    {
                        Thread.Sleep(100);
                        Console.Write(i + ",");
                    }
                    Console.WriteLine();
                }
                else
                {
                    Console.WriteLine(Thread.CurrentThread.Name + " 对不起,没能获取到锁,真实丢脸");
                }
            }
            finally
            {
                if(isLockTaken)
                {
                    Monitor.Exit(locker);
                    Console.WriteLine(Thread.CurrentThread.Name + " Exit from critical section");
                }
            }
        }
    }

由于每个线程运行时间大概时500微秒,而每个线程的等待时间都是1000微妙,那么最后一个线程自然无法在规定时间内获得锁。让我运行看看结果:

Child Thread 0 Trying to enter into the critical section
Child Thread 1 Trying to enter into the critical section
Child Thread 2 Trying to enter into the critical section
Child Thread 0 Entered into the critical section
0,1,2,3,4,
Child Thread 0 Exit from critical section
Child Thread 1 Entered into the critical section
0,1,2,3,Child Thread 2 对不起,没能获取到锁,真实丢脸
4,
Child Thread 1 Exit from critical section

显然1000微妙过后,未获得锁的线程2自爆了,此时线程1正在打印最后一个数字。验证了我们之前的推断。

6. Wait()和Pulse()

6.1 轮流打印问题

考虑一个面试题,给你一个数组(比如自然数1-20)用两个线程打印出来,而且必须是轮流打印。

我们利用前面的Enter和Exit函数,让两个线程轮流进入和退出以此来实现轮流打印,代码如下:

    internal class InTurnPrint
    {
        public static int Max= 20;
        public static object locker=new object();

        public static void Run()
        {
            Thread[] threads = new Thread[2];
            threads[0] = new Thread(Print) { Name = "Even" };
            threads[1] = new Thread(Print) { Name = "Odd" };
            Array.ForEach(threads, t => { t.Start(); t.Join(); });
            Console.WriteLine("-----------");
        }

        public static void Print()
        {
            while(Max>0)
            {
                try
                {
                    Monitor.Enter(locker);
                    Console.WriteLine(Max--+" ---- "+Thread.CurrentThread.Name);
                }
                finally
                {
                    Monitor.Exit(locker);
                    Thread.Sleep(100);
                }
            }
            Console.WriteLine(Thread.CurrentThread.Name + " Exit!");
        }
    }

实际运行结果如下:

20 ---- Even
19 ---- Even
18 ---- Even
17 ---- Even
16 ---- Even
15 ---- Even
14 ---- Even
13 ---- Even
12 ---- Even
11 ---- Even
10 ---- Even
9 ---- Even
8 ---- Even
7 ---- Even
6 ---- Even
5 ---- Even
4 ---- Even
3 ---- Even
2 ---- Even
1 ---- Even
Even Exit!
Odd Exit!
-----------

显然,另一个线程一直没有获得锁,即使我们在释放锁之后加了延迟(即使在循环开始加上延迟也是一样)。显然加延迟是没用的,总是由第一个线程获得锁。所以仅靠之前的几个函数无法实现,这就要介绍另外两个函数了。

6.2 Wait和Pulse介绍

1. Monitor.Wait 方法

有两个比较常用的方法重载:

Monitor.Wait(Object)

Object:等待的锁的对象

功能:释放当前线程所占用的对象锁,并且阻塞当前的线程直到它再次拥有这个锁。

Releases the lock on an object and blocks the current thread until it reacquires the lock.

Monitor.Wait(Object,Int32)

- Object:等待的锁的对象

- Int32:线程再次进入就绪队列的等待时长,单位毫秒

功能:释放当前线程所占用的对象锁,并且阻塞当前的线程直到它再次拥有这个锁。如果指定的时长过去,线程将由等待队列转移到就绪队列。

2. Monitor.Wait 方法的主要执行步骤

阻塞当前的线程

将这个线程移动到等待队列中

释放当前的同步锁

3. Monitor.Pulse (Object)

功能:通知一个等待队列中的线程,当前锁的状态被改变。(说白了就是有一个线程可以从等待队列中被移入就绪队列)

Notifies a thread in the waiting queue of a change in the locked object's state.

4. Monitor.PulseAll(Object)

功能:通知所有的等待队列中的线程,当前锁的状态改变。(说白了就是所有的线程可以从等待队列中被移入就绪队列)

Notifies all waiting threads in the waiting queue of a change in the locked object's state.

5. Monitor.Pulse 和 Monitor.PulseAll 的使用写法:

只能由当前获得锁的线程,调用 Monitor.Pulse 和 Monitor.PluseAll 后,使等待队列中的线程转义到就绪队列。

6.3 机制分析

6.3.1 情形一

  1. 假设有五个线程,t1,t2,t3,t4,t5,他们同事启动进入Lock区域,如下:

lock(obj)  //这里lock 等价于Enter、Exit组合
{
    Monitor.Wait(obj);
}
  1. 由于线程t1被第一个处理,进而进入了Lock,它获得锁,此时所有线程的状态:

拥有线程的锁

t1

就绪队列

t2,t3,t4,t5

等待队列

  1. 假设线程 t1 运行到了 Monitor.Wait,它将会被从拥有线程锁状态移动到等待队列状态中,于此同时将会释放其拥有的锁,而其它在就绪队列中的线程将有机会获得这个锁:

拥有线程的锁

就绪队列

t2,t3,t4,t5

等待队列

t1

  1. 此时假设线程 t2 获取了 t1 释放的锁,它将进入 lock 区域中,此时所有的线程状态如下:

拥有线程的锁

t2

就绪队列

t3,t4,t5

等待队列

t1

  1. 接着 t2 在 lock 区域中也会执行 Monitor.Wait ,之后 t2 也会像 t1 一样进入等待队列,重复 1、2 步骤,直至所有的线程 t1、t2、t3、t4、t5 都进入等待队列,如下图:

拥有线程的锁

就绪队列

等待队列

t1,t2,t3,t4,t5

6.3.2 情形二

如何将上面等待队列中的某一个线程重新变为就绪状态,从而可以再次拿到锁呢?

答:我们可以使用 Monitor.Pulse 来让 t1 线程从等待队列中转移到就绪队列中。

★★ 这里有一个需要注意的地方,就是 " 等待队列 " 是一个队列,满足 " 先进先出 ",所以第一个线程 t1 会被优先释放到就绪队列中。

  1. 我们在情形一第5点的状态下执行 Monitor.Pulse,此时所有的线程的状态如下:

拥有线程的锁

就绪队列

t1

等待队列

t2,t3,t4,t5

  1. 然后,线程 t1 在就绪队列中就会拿到锁,从 Monitor.Wait 的下一句程序开始执行:

拥有线程的锁

t1

就绪队列

等待队列

t2,t3,t4,t5

  1. 最后,t1 线程在执行完 lock 区域的剩余部分的代码之后就会退出,同时释放线程锁。于此同时,其它的线程依然被卡在等待队列中等待,如下:

拥有线程的锁

就绪队列

等待队列

t2,t3,t4,t5

  1. 对于 Monitor.PulseAll 将会把所有的等待状态的线程都移到就绪状态的队列中,从而有机会获得锁进行执行。从第3步接着执行 Monitor.PulseAll 之后,所有的线程状态如下:

拥有线程的锁

就绪队列

t2,t3,t4,t5

等待队列

6.4 实现轮流打印问题

Wait()和Pulse()为我们提供了很好的思路。我们先看第一种实现:

我们写两个函数,分别打印数组:

 internal class MonitorStudy
    {
        const int numberLimit = 21;
        static readonly object _locker=new object();

        public static void Run()
        {
            Thread evenThread=new Thread(PrintEvenNumbers) { Name="Even"};
            Thread oddThread=new Thread(PrintOddNumbers) { Name="Odd"};   
            evenThread.Start();
            oddThread.Start();
            evenThread.Join();
            oddThread.Join();
            Console.WriteLine("-----------------");
        }
        static void PrintEvenNumbers()
        {
            try
            {
                Monitor.Enter(_locker);
                for(int i=0; i<numberLimit; i++)
                {
                    if(i%2==0)
                    {
                        Console.WriteLine($"{i} -----{Thread.CurrentThread.Name}");
                    }
                    //Notify Odd thread that I'm done, you do your job
                    //It notifies a thread in the waiting queue of a change in the 
                    //locked object's state.
                    Monitor.Pulse(_locker);
                    //Notify Odd thread that I'm done, you do your job
                    //It notifies a thread in the waiting queue of a change in the 
                    //locked object's state.
                    if (i + 1 == numberLimit)
                        break;
                    Monitor.Wait(_locker);
                }
            }
            finally
            {
                Monitor.Exit(_locker);
            }
        }

        static void PrintOddNumbers()
        {
            try
            {
                Monitor.Enter(_locker);
                for(int i=0;i<numberLimit;i++)
                {
                    if(i%2!=0)
                    {
                        Console.WriteLine($"{i} -----{Thread.CurrentThread.Name}");
                    }
                    Monitor.Pulse(_locker);
                    if (i + 1 == numberLimit)
                        break;
                    Monitor.Wait(_locker);
                }
            }
            finally
            {
                Monitor.Exit(_locker);
            }
        }
    }

这里值得注意的是上面的两端注释,第一段注释是让另一个线程进入就绪队列,一旦当前线程调用wait释放锁,则另一个线程立即执行。第二个要点是,一旦循环到末位,有一个线程会直接退出,此时另一个线程由于调用Wait还在等待中,这会导致阻塞,所以这里有一个判断,如果是边界值,则不用再等待了,结束整个程序。

上面的程序,我们做了奇偶判断,好像显得很繁琐,前面我们讨论了两种情形,理论上,当线程释放时,必然会被别的线程先获取,所以有且只有两个线程时,可以不用进行奇偶判断,于是我们可以只写一个函数,让两个线程都调用即可。实现如下:

internal class InTurnPrint
    {
        public static int Max= 20;
        public static object locker=new object();

        public static void Run()
        {
            Thread[] threads = new Thread[2];
            threads[0] = new Thread(Print) { Name = "Even" };
            threads[1] = new Thread(Print) { Name = "Odd" };
            Array.ForEach(threads, t => { t.Start(); t.Join(10); });
            Console.WriteLine("-----------");
        }

        public static void Print()
        {
            while(Max>0)
            {
                lock(locker)
                {
                    Console.WriteLine(Max-- + " ---- " + Thread.CurrentThread.Name);
                    Monitor.Pulse(locker);
                    if(Max!=0)
                        Monitor.Wait(locker);
                }              
            }
            Console.WriteLine(Thread.CurrentThread.Name + " Exit!");
        }
    }

注意线程调用Join是为了确保子线程先结束,如果不传入参数,Join会阻塞UI打印。另一种办法是不加Join,在第12行前面加一个:

Thread.sleep(100)

也可以实现同样的效果。

7. 总结

Monitor类和Lock的区别在于:Lock内部实际是将Monitor类的Enter和Exit函数放入Try……finally模块中,并添加异常处理,因此我们使用Monitor类时需要显示的使用try和finally模块来讲锁显示的释放

Lock=Monitor+try-finally

Lock语句通过同步对象提供一个基本的排它锁,但是如果你想跟精细的控制并行程序,就需要使用TryEnter(),Wait(),Pulse()和PulseAll()函数,此时使用Monitor类能满足你更好的需求。

锁和监视器帮助我们确保我们的代码是线程安全的。这意味着当我们在多线程环境中运行我们的代码时,我们不会得到不一致的结果。为了更好地理解,请看下图。

但是锁和监视器有一些限制。锁和监视器确保 In-Process 线程的线程安全,即由应用程序本身生成的线程,即内部线程。但是,如果线程来自外部应用程序(Out-Process)或外部线程,那么 Locks 和 Monitors 将无法控制它们。所以,在这种情况下,我们需要使用 Mutex。在我们的下一篇文章中,我们将讨论 Mutex。

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

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

相关文章

Microsoft Visual SourceSafe的使用

1、介绍 Microsoft Visual SourceSafe&#xff0c;简称vss。是一款早期微软推出的版本管理工具。跟据官方的定义&#xff0c;vss有两种控制模式&#xff1a;独占&#xff08;Lock-Modify-Unlock Model&#xff09;和并行&#xff08;Copy-Modify-Merge Model&#xff09;。独占…

程序的安装——软件安装包的制作、软件源的使用

读书笔记 —— 《嵌入式C语言自我修养》 软件安装 linux 安装包的制作 编译 软件安装包路径 使用dpkg命令来制作安装包 及 安装包的卸载 软件仓库 更新源 查看具体需要更新的软件包 更新软件包 软件安装 软件安装的过程其实就是将一个可执行文件安装到ROM的过…

安全狗云原生安全从1.X到2.X的演变之路(1)

随着云计算技术的蓬勃发展&#xff0c;传统上云实践中的应用升级缓慢、架构臃肿、无法快速迭代等“痛点”日益明显。能够有效解决这些“痛点”的云原生技术正蓬勃发展&#xff0c;成为赋能业务创新的重要推动力&#xff0c;并已经应用到企业核心业务。然而&#xff0c;云原生技…

大型数据中心分层分布式谐波治理方案设计与效果分析

摘要&#xff1a;数据中心行业在国民经济中起到了不可替代的作用,但其繁多的非线性电力负载,如通讯系统、大型计算机、网络控制设备、变频空调、各种数码办公设备、灯光调控系统、UPS、监控系统等给其供电系统带来了严重的谐波干扰,对大型数据中心的运行安全造成了较大的威胁,为…

200:vue+openlayers 添加删除多边形,modify feature,双向互动颜色显示

第200个 点击查看专栏目录 本示例的目的是介绍如何在vue+openlayers项目中添加删除多边形,每绘制一个,左侧列表出一个信息。 hover左侧文字,右边地图上红色显示图形,点击选中右侧地图上某feature,变成蓝色高亮,同时左侧也会蓝色显示,做到双向互动。 高亮显示某feature,…

如何删除图片数据中的重复数据

我们在工作中经常这种情况&#xff0c;leader给你一堆数据&#xff0c;让你用这些没有清洗过的数据完成项目。痛苦的是&#xff0c;这批数据居然存在很多重复的样本。那如何删除这些冗余数据呢&#xff1f;imagehash库非常好用。 github地址&#xff1a;https://github.com/ch…

络达开发----如何开启DMIC

芯片型号&#xff1a;AB1565 功能模块&#xff1a;数字MIC接口的使用 AB1656评估板上支持两路数字MIC&#xff0c;分别为DMIC0和DMIC1&#xff0c;如果图1所示&#xff0c;分别 可以由GPIO_2/3/4/5/13/14/15/16来当数字MIC的接口。 图1&#xff1a;支持DMIC的IO口但是评估板上…

MAC M1使用Rosetta安装python3.6

在使用网上提到的brew和pyenv安装的时候&#xff0c;我的电脑总会报BUILD FAILED错误。 找了一天才找到解决办法&#xff0c;真的十分痛苦&#xff0c;特此记录一下&#xff0c;让别的小伙伴也不再迷茫。 解决办法参考网址&#xff1a;click here&#xff08;需要VPN&#xff…

html跑马灯走马灯效果

演示 <marquee width"100%" scrollamount"5"> <a href"http://www.taobaojp5.tk"><font face"楷体_GB2312" color"#ff0000" size"3"></font><strong>带有超链接的跑马灯!点我试试&…

uwsgi 快速入门

文章目录uwsgi 快速入门一、 概述1、 简单介绍2、 环境配置二、 第一个 WSGI 应用1、 运行2、 添加并发三、 结合 Web 服务器使用1、 Flask2、 Django3、 Nginx配置uwsgi 快速入门 一、 概述 1、 简单介绍 WSGI&#xff08;Web Server Gateway Interface&#xff09;&#x…

FPGA知识汇集-ASIC移植中的FPGA芯片划分

通常&#xff0c;FPGA单芯片难以容纳下整个ASIC设计&#xff0c;因此需要将整个系统划分到多颗FPGA芯片中运行&#xff08;见图1&#xff09;&#xff0c;工程师往往需要借助原型验证平台来实现这样的目标。多芯片的划分绝不是简单的将不同的模板放置到不同的FPGA中那么简单&am…

算法训练营 day16 二叉树 二叉树的最大深度 二叉树的最小深度 完全二叉树的节点个数

算法训练营 day16 二叉树 二叉树的最大深度 二叉树的最小深度 完全二叉树的节点个数 二叉树的最大深度 104. 二叉树的最大深度 - 力扣&#xff08;LeetCode&#xff09; 给定一个二叉树&#xff0c;找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点…

(JVM) 沙箱安全机制

沙箱安全机制 沙箱安全机制 保证程序安全 保护Java原生的JDK代码 Java安全模型的核心就是Java沙箱&#xff08;sandbox&#xff09;。什么是沙箱&#xff1f;沙箱是一个限制程序运行的环境。 沙箱机制就是将Java代码限定在虚拟机&#xff08;JVM&#xff09;特定的运行范围…

Javaweb+Vue开发中常见的问题-1

1.Vue乱码问题 Vue2 修改打包文件的编码格式&#xff08;webpack-encoding-plugin&#xff09;_一碗单炒饭的博客-CSDN博客_vue如何设置打包文件的编码格式 怎么把命令行改成utf_8 - 沿途百知 设置命令行的编码格式&#xff1a;chcp 65001 https://bbs.csdn.net/topics/3935426…

基于微信小程序云开发的职业学校招生报名小程序源码,职业学校招生报名微信小程序源码 ,职业学校招生报名小程序源码

功能介绍 这是一个以报名为核心的职业学校招生小程序&#xff0c;目的是方便想要系统学习技能&#xff0c;入门某项技能或者领域的初高中毕业生&#xff0c;了解该学校的基本情况及各个专业&#xff0c;并提供报名路径&#xff0c;致力于技能型人才培养。本程序前后端代码完整…

ikun运球新姿势-- 反弹shell

目录 反弹Shell 反弹shell的概述 正向连接 反向连接 为什么需要反弹shell 利用netcat反弹shell 利用Bash反弹shell curl配合Bash反弹shell 将反弹shell的命令写入定时任务 将反弹shell的命令写入/etc/profile文件 python脚本反弹shell 反弹Shell 反弹shell的概述 …

免费的移动硬盘数据恢复软件EasyRcovery15

在日常工作中&#xff0c;移动硬盘可以帮助用户存储重要的文件资料&#xff0c;作为可移动的存储设备&#xff0c;在外出工作时携带起来也比较的方便&#xff0c;而且它的存储空间大&#xff0c;不会出现数据文件过大而无法储存的情况。今天小编就来和大家分享一下&#xff0c;…

算能杯|全国大学生集成电路创新创业大赛开启报名!

第七届全国大学生集成电路创新创业大赛正式开幕&#xff0c;“算能杯”主题是基于TPU芯片的边缘计算系统设计&#xff0c;算能为参赛选手提供了超强算力的开发板、无人机、人工智能小车等硬件资源&#xff0c;欢迎各大高校的开发者报名参与&#xff01; 近几年&#xff0c;边缘…

4-选择题练手

1.在Java中&#xff0c;以下关于方法重载和方法重写描述正确的是A. 方法重载和方法的重写实现的功能相同 B. 方法重载出现在父子关系中&#xff0c;方法重写是在同一类中 C. 方法重载的返回值类型必须一致&#xff0c;参数项必须不同D.方法重写的返回值类型必须相同或相容答&am…

05.rocketmq源码分析后的一些整理

经过近3年的打磨&#xff0c;我们自研的企业基础应用框架基本成型&#xff0c;并且在多家规模企业中全面落地&#xff0c;从最新的统计数据来看&#xff0c;经过一年的上线应用&#xff0c;某省港集团在平台中管理的运营流程接近600/审批事项接近73w&#xff0c;业务消息量就更…