多线程经典代码案例及手动实现

目录

一.线程和多线程

二. 多线程的经典的代码案例

1.单例模式

2.阻塞队列

(1)概念介绍

(2)生产者消费者模型

(3)手动实现阻塞队列

(4)代码解释及问题分析

3.定时器

(1)概念介绍

(2)思路分析

(3)手动实现定时器

(4)代码解释及问题分析

问题一:优先级

问题二 :忙等

问题三 :加锁

4.线程池

(1)概念介绍

(2)具体分析 

 (3)手动实现线程池

 (4)代码解释及问题分析

问题一:变量捕获

问题二:线程数量 

三. 总结——保证线程安全的思路


一.线程和多线程

我们都知道,线程有很多优点
线程的优点
1. 创建一个新线程的代价要比创建一个新进程小得多
2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3. 线程占用的资源要比进程少很多
4. 能充分利用多处理器的可并行数量
5. 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务
6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

因此我们在实际开发中,经常采用多线程编程. 

而多线程有几个经典的代码案例

  • 单例模式
  • 阻塞队列
  • 定时器
  • 线程池

记下来我们就进行具体分析.

二. 多线程的经典的代码案例

1.单例模式

单例模式在我的另一篇博文中已经进行了介绍

工厂模式和单例模式

2.阻塞队列

(1)概念介绍

阻塞队列是一种特殊的队列 . 也遵守 " 先进先出 " 的原则 .
阻塞队列能是一种线程安全的数据结构 , 并且具有以下特性 :
当队列满的时候, 继续入队列就会阻塞 , 直到有其他线程从队列中取走元素 .
当队列空的时候 , 继续出队列也会阻塞 , 直到有其他线程往队列中插入元素 .
阻塞队列的一个典型应用场景就是 " 生产者消费者模型 ". 这是一种非常典型的开发模型 .

(2)生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
  • 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
  • 阻塞队列也能使生产者和消费者之间 解耦.
Java 标准库中内置了阻塞队列 . 如果我们需要在一些程序中使用阻塞队列 , 直接使用标准库中的即可 .
BlockingQueue 是一个接口 . 用它来使用阻塞队列
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列
我们来看一个代码例子:
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;public class ThreadDemo19 {public static void main(String[] args) throws InterruptedException {BlockingDeque<String>quene=new LinkedBlockingDeque<>();//阻塞队列的核心方法,主要有两个//1.put 入队列quene.put("hello1");quene.put("hello2");quene.put("hello3");quene.put("hello4");quene.put("hello5");//2.take 出队列String result=null;result=quene.take();System.out.println(result);result=quene.take();System.out.println(result);result=quene.take();System.out.println(result);result=quene.take();System.out.println(result);result=quene.take();System.out.println(result);result=quene.take();System.out.println(result);}
}

运行结果如下:

而基于阻塞队列实现的"生产者消费者模型"代码如下:

import java.util.concurrent.*;
import java.util.concurrent.BlockingQueue;//基于阻塞队列写生产者-消费者模型
public class ThreadDemo20 {public static void main(String[] args) {BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();//生产者Thread t1=new Thread(()->{while (true){try {int value=blockingQueue.take();System.out.println("消费元素:"+value);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();//消费者Thread t2=new Thread(()->{int value=0;while (true){try {blockingQueue.put(value);value++;Thread.sleep(1000);System.out.println("消费元素:"+value);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t2.start();}
}

运行结果如下:

(3)手动实现阻塞队列

接下来,我们来学习如何自己手动实现阻塞队列
class MyBlockingQueue{private int[] item =new int[1000];//约定[head,tail)队列的有效元素volatile private int head=0;volatile private int tail=0;volatile private int size=0;//入队列synchronized public void put(int elem) throws InterruptedException {while(size== item.length){//队列满了,插入失败//return;this.wait();}//把新元素放在tail所在的位置上item[tail]=elem;tail++;//万一tail达到末尾,就需要让tail从头再来if(tail==item.length){tail=0;}//tail=tail%item.length //可以但不推荐size++;this.notify();}//出队列synchronized public Integer take() throws InterruptedException {while(size==0){//return null;this.wait();}int value=item[head];head++;if(head==item.length){head=0;}size--;this.notify();return value;}}
  1. 通过 "循环队列" 的方式来实现.
  2. 使用 synchronized 进行加锁控制.
  3. put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
  4. 定队列就不满了, 因为同时可能是唤醒了多个线程).
  5. take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)

(4)代码解释及问题分析

这里大家可能会有疑问,为什么在进行队列判定的时候,我们使用的是while,用if来判定不可以吗?

接下来,我们就来做出解释:

但是Java官方并不建议这么使用wait,我们点进wait的源码来看看

而我们写的代码很有可能在别的部分中暗中 interrupt,把 wait 给提前唤醒了,明明条件还没满足(队列非空),但是 wait 唤醒之后就继续往下走了.

当然,我们当前的这个简单的实例代码中,没有 interrupt,但是一个更复杂的项目,就不能保证没有了.

更稳妥的做法是在 wait 晚醒之后,再判定一次条件.

wait 之前,发现条件不满足,开始 wait,然后等到 wait 被唤醒了之后,再确认一下条件是不是满足.如果不满足,还可以继续 wait .

 这个时候,我们就可以将判定条件改成while来进行判定,就可以使代码更完善了.

3.定时器

(1)概念介绍

定时器也是软件开发中的一个重要组件 . 类似于一个 " 闹钟 ". 达到一个设定的时间之后 , 就执行某个
指定好的代码.
定时器是一种实际开发中非常常用的组件 .
比如网络通信中 , 如果对方 500ms 内没有返回数据 , 则断开连接尝试重连 .
比如一个 Map, 希望里面的某个 key 3s 之后过期 ( 自动删除 ).
类似于这样的场景就需要用到定时器 .
  • 标准库中提供了一个 Timer . Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
//定时器
import java.util.Timer;
import java.util.TimerTask;public class ThreadDemo22 {public static void main(String[] args) {Timer timer=new Timer();timer.schedule(new TimerTask(){@Overridepublic void run() {System.out.println("hello2");}},2000);System.out.println("hello1");}
}
  • 这里的TimerTask()本质上就是Runnable()
  • 而打印hello2的执行是靠Timer内部的线程在时间到了之后执行的.即2秒之后执行run方法

既然定时器的应用这么多,那我们该如何自己实现一个定时器呢?

(2)思路分析

首先,我们来进行分析

  • 定时器,内部管理的不仅仅是一个任务,它可以管理很多任务.

所以我们的核心数据结构就是使用堆.

  • 而且,虽然任务可能有很多,他们的触发的时闻是不同的,只需要有一个/一组工作线程,每次都找到这些任务中最先到达时间的任务.一个线程先执行最早的任务,做完了之后再执行第二早的... 时间到了就执行,没到就等待.

正因如此,我们就要使用带优先级的阻塞队列PriorityQueue来实现.

同时,定时器里可能会有多个线程在执行shedule方法,因此我们也希望在多线程下操作优先级队列也能保证线程安全. 

(3)手动实现定时器

 代码如下:

/**
* 定时器的构成:
* 一个带优先级的阻塞队列
* 队列中的每个元素是一个 Task 对象.
* Task 中带有一个时间属性, 队首元素就是即将
* 同时有一个 t 线程一直扫描队首元素, 看队首元素是否需要执行
*/import java.util.concurrent.PriorityBlockingQueue;class MyTask implements Comparable<MyTask>{public Runnable runnable;public long time;public MyTask(Runnable runnable,long delay){this.runnable=runnable;//取当前时刻的时间戳+delay作为该任务实际执行的时间戳this.time=System.currentTimeMillis()+delay;//这里的currentTimeMillis是ms级别的时间戳,是当前时刻和基准时刻的ms数之差}@Overridepublic int compareTo(MyTask o) {return (int)(this.time-o.time);}}class MyTimer{//这个结构,带有优先级的阻塞队列,核心数据结构private PriorityBlockingQueue<MyTask> quene=new PriorityBlockingQueue<>();//手动封装//创建个例,表示两方面信息//1.执行的任务是什么//2.任务什么时候开始执行private Object Locker=new Object();//schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒).public  void schedule(Runnable runnable,long delay){//根据参数,构造MyTask,插入队列即可MyTask myTask=new MyTask(runnable,delay);quene.put(myTask);synchronized (Locker){Locker.notify();}}//构造线程,负责执行具体任务public MyTimer() {Thread t=new Thread(()->{while(true){// synchronized (Locker){try {//阻塞队列,只有阻塞的入队列和阻塞的出队列,没有阻塞的查看队首元素MyTask myTask=quene.take();long CurTime=System.currentTimeMillis();if(myTask.time<=CurTime){//时间到了,可以执行任务了myTask.runnable.run();}else {//时间还没到//把刚才取出的任务,重新塞回队列中quene.put(myTask);synchronized (Locker){Locker.wait(myTask.time-CurTime);}}} catch (InterruptedException e) {throw new RuntimeException(e);}}// }});t.start();}}public class ThreadDemo23 {public static void main(String[] args) {// System.out.println(System.currentTimeMillis());MyTimer myTimer=new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello4");}},4000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello3");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello2");}},2000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello1");}},1000);System.out.println("hello0");}
}
  • Timer 类提供的核心接口为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行.
  • Task 类用于描述一个任务(作为 Timer 的内部类). 里面包含一个 Runnable 对象和一个 time(毫秒时间戳)
        这个对象需要放到 优先队列 中 . 因此需要实现 Comparable 接口 .
  • Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象.
        通过 schedule 来往队列中插入一个个 Task 对象.
  • Timer 类中存在一个 t 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.

(4)代码解释及问题分析

而这段代码里,有几个值得我们思索的问题:

问题一:优先级

1.当前队列里的 MyTask 元素是按照什么规则来表示优先级的?

按照我们的分析

因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 

 因此我们比较时间来进行排序

static class Task implements Comparable<Task> {private Runnable command;private long time;public Task(Runnable command, long time) {this.command = command;// time 中存的是绝对时间, 超过这个时间的任务就应该被执行this.time = System.currentTimeMillis() + time;}public void run() {command.run();}@Overridepublic int compareTo(Task o) {// 谁的时间小谁排前面return (int)(time - o.time);}}
}
问题二 :忙等

2.当前这个代码中存在一个严重的问题, 就是 while (true) 转的太快了, 造成了无意义的 CPU 浪费. 也就是忙等.

比如第一个任务设定的是 1 min 之后执行某个逻辑 . 但是这里的 while (true) 会导致每秒钟访问队 首元素几万次. 而当前距离任务执行的时间还有很久呢 .

 那么该如何解决呢?

我们需要引入一个新的 对象 , 借助该对象的 wait / notify 来解决 while (true) 的忙等问题 .
class Timer {// 存在的意义是避免 t 线程出现忙等的情况private Object Locker = new Object(); 
}
(1)修改 t  run 方法, 引入 wait, 等待一定的时间.
public void run() {while (true) {try {Task task = queue.take();long curTime = System.currentTimeMillis();if (task.time > curTime) {// 时间还没到, 就把任务再塞回去queue.put(task);// [引入 wait] 等待时间按照队首元素的时间来设定. synchronized (Locker) {// 指定等待时间 waitLocker.wait(task.time - curTime);}} else {// 时间到了, 可以执行任务task.run();}} catch (InterruptedException e) {e.printStackTrace();break;}}
}

(2)修改 Timer schedule 方法, 每次有新任务到来的时候唤醒一下 t 线程. (因为新插入的任务可能是需要马上执行的).

public  void schedule(Runnable runnable,long delay){MyTask myTask=new MyTask(runnable,delay);quene.put(myTask);// [引入 notify] 每次有新的任务来了, 都唤醒一下 t 线程, 检测下当前是否有新任务synchronized (Locker){Locker.notify();}}

 这里使用wait来等待而不是sleep,因为wait方便随时提前唤醒.

wait的参数是"超时时间",时间达到一定数值之后,还没有被notify就不再等待,如果时间还没到就被notify,就立即返回.

问题三 :加锁

3.synchronized()的使用范围.

这里为什么将加锁位置改到了这里而不是全部加锁?

我们知道,加锁后可以使某部分代码变成具有原子性的代码.这里假如我们为全部这部分代码加锁,假如在中间插入一个新的线程,那么有没有可能发生特殊情况呢?

当然是有的.

这是一种矛盾的状态,因此是有bug的,所以我们把代码进行了修改.我们把锁加在wait外面.

 

此时它的take和wait操作就都是原子的了.我们再进行分析.

 因此,把锁加在wait外面才是更安全的.

4.线程池

(1)概念介绍

想象这么一个场景:
在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人, 而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。
这个类比我们平时来 一个任务,起一个线程进行处理的模式。
很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知 道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。

 线程池最大的好处就是减少每次启动、销毁线程的损耗.

因为从线程池取线程,是纯用户态操作,不涉及到和内核的交互.

标准库中的线程池
  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadDemo24 {public static void main(String[] args) {//线程池ExecutorService pool= Executors.newFixedThreadPool(10);pool.submit(new Runnable(){@Overridepublic void run() {System.out.println("hello");}});}
}

(2)具体分析 

我们来分析给出的文档: 

 同样,标准库里也提供了四种拒绝策略

 (3)手动实现线程池

 接下来,我们就来尝试自己手动实现线程池.

代码如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;class MyThreadPool{//产生一个阻塞队列private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();//submit相当于一个生产者,往阻塞队列里面添加任务public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}//相当于消费者,不断地取任务,然后进行执行public  MyThreadPool(int n){for(int i=0;i<n;i++){Thread t=new Thread(()->{try {while (true){//此处需要让线程内部有个while循环,不断地取任务Runnable runnable= queue.take();runnable.run();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}}
}
public class ThreadDemo25 {public static void main(String[] args) throws InterruptedException {MyThreadPool pool=new MyThreadPool(10);//创建出10个线程//每次循环都是创建一个新number,没有人修改该numberfor (int i = 0; i < 1000; i++) {int number=i;//直接用i不行,用number是因为匿名内部类需要捕获外部的变量,这里要求变量是final的,而此处的i是不断地被修改的// 因此我们需要创建另一个变量,把它变成事实final,就可以被捕获了pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("HELLO"+number);}});}}
}

运行代码如下:

此处可以看到,线程池中任务执行的顺序和添加顺序不一定相同的.

这非常正常,因为这些线程是无序调度的.

 (4)代码解释及问题分析

接下来,我们来分析代码中的一些要点.

问题一:变量捕获

1.这里为什么要用number来接收,直接使用i不可以吗?

 直接用i不行,用number是因为匿名内部类需要捕获外部的变量,这里要求变量是final的,而此处的i是不断地被修改的.


因此我们需要创建另一个变量,把它变成事实final,就可以被捕获了.

问题二:线程数量 

2. 当前代码中,我们创建了个十个线程的线程池.那么实际开发中,一个线程池的线程数量,设置成几是比较合适的?

我们之前说,线程不是越多越好,因为线程本质上还是要在CPU上执行调度.

网上有很多说法.比如假设 cpu 核心数是 N,线程池的数目,设置成 N,N + 1,2N,15N.... 有很多个说法的版本.

但是实际上,不同的程序,线程做的工作也不一样.

  • CPU密集型任务.主要做一些计算工作.要在 cpu 上运行的
  • I/O 密集型任务.主要是等待 IO 操作(等待读硬盘,读写网卡)

⌛极端情况,如果你的线程全是使用 cpu,线程数就不应该超过 cpu 核心数
⌛如果你的线程全是使用I/O,线程数就可以设置很多, 远远超出 cpu 核心数

然而实践中很少有这么极端的情况,具体要通过测试的方式来确定.取一个执行效率比较高并且占用资源也合适的数量.

 

三. 总结——保证线程安全的思路

📢使用没有共享资源的模型
📢适用共享资源只读,不写的模型
  • 不需要写共享资源的模型
  • 使用不可变对象
📢直面线程安全
  • 保证原子性
  • 保证顺序性
  • 保证可见性

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SSM的电动车上牌管理系统(有报告)。Javaee项目。

演示视频&#xff1a; 基于SSM的电动车上牌管理系统&#xff08;有报告&#xff09;。Javaee项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringM…

Java基本数据类型和变量

目录 一、基本数据类型 1.1 整型 1.1.1 byte 1.1.2 short 1.1.3 int 1.1.4 long 1.2 浮点型 1.2.1 float 1.2.2 double 1.3 字符型 1.4 布尔型 二、变量 2.1 变量的概念 2.2 语法格式 2.3 整型变量 2.3.1 整型变量 2.3.2 长整型变量 2.3.3 短整型变量 2.3.…

MySQL之DQL

DQL是数据查询语言 SELECT语句 语法&#xff1a; SELECT {*,列名&#xff0c;函数等} FROM 表名;SELECT *&#xff1a;表示匹配所有列 FROM :提供数据源 例如:查询student表的所有记录 SELECT * FROM student;例如&#xff1a;查询学生姓名和地址&#xff1a; SELECT Stud…

学信息系统项目管理师第4版系列16_资源管理过程

1. 组建项目团队&#xff0c;建设项目团队和管理项目团队属于执行过程组 1.1. 【高22上选21】 1.1.1. 【高21上选25】 1.2. 3版 2. 【高19上案三】 2.1. 【高18上案三】 2.2. 【高23上案一】 3. 规划资源管理 3.1. 定义如何估算、获取、管理和利用团队以及实物资源的过…

mstsc无法保存RDP凭据, 100%生效

问题 即使如下两项都打勾&#xff0c;其还是无法保存凭据&#xff0c;特别是连接Ubuntu (freerdp server)&#xff1a; 解决方法 网上多种复杂方法&#xff0c;不生效&#xff0c;其思路是修改后台配置&#xff0c;以使mstsc跟平常一样自动记住凭据。最后&#xff0c;如下的…

斯坦福数据挖掘教程·第三版》读书笔记(英文版)Chapter 10 Mining Social-Network Graphs

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT。 Chapter 10 Mining Social-Network Graphs The essential characteristics of a social network are: There is a collection of entities that participate in the network. Typically, these entiti…

Python学习笔记之分支结构与循环结构

Python学习笔记之分支结构与循环结构 一、分支结构 使用关键字if、elif、else 练习1&#xff1a;使用分支结构实现分段函数求值 """分段函数求值""" x float(input("x "))if x > 1:y 3 * x - 5 elif x < -1:y 5 * x 3…

2023/10/4 -- ARM

今日任务&#xff1a;QT实现TCP服务器客户端搭建的代码&#xff0c;现象 ser&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);server new QTcpSe…

免费、丰富、便捷的资源论坛——Yiove论坛,包括但不限于阿里云盘、夸克云盘、迅雷云盘等等

引言 目前资源的数量达到了60000&#xff0c;六万多的资源意味着在这里几乎可以找到任何你想要的资源。 当然&#xff0c;资源并不是论坛的全部&#xff0c;其中还包括了技术交流、福利分享、最新资讯等等。 传送门&#xff1a;YiOVE论坛 - 一个有资源有交流&#xff0c;有一…

PCL 计算点云中值

目录 一、算法原理2、主要函数二、代码实现三、结果展示四、参考链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 计算点云坐标的中值点,首先对点云坐标进行排序,然后计算中值。如果点云点的个数为奇数…

计组—— I/O系统

&#x1f4d5;&#xff1a;参考王道课件 目录 一、I/O系统的基本概念 1.什么是“I/O”&#xff1f; ​编辑2.主机如何和I/O设备进行交互&#xff1f; 3.I/O控制方式 &#xff08;1&#xff09;程序查询方式 &#xff08;2&#xff09;程序中断方式 &#xff08;3&#x…

号卡推广管理系统源码/手机流量卡推广网站源码/PHP源码+带后台版本+分销系统

源码简介&#xff1a; 号卡推广管理系统源码/手机流量卡推广网站源码&#xff0c;基于PHP源码&#xff0c;而且它是带后台版本&#xff0c;分销系统。运用全新UI流量卡官网系统源码有后台带文章。 这个流量卡销售网站源码&#xff0c;PHP流量卡分销系统&#xff0c;它可以支持…

C#餐饮收银系统

一、引言 餐饮收银系统是一种用于管理餐馆、咖啡厅、快餐店等餐饮业务的计算机化工具。它旨在简化点餐、结账、库存管理等任务&#xff0c;提高运营效率&#xff0c;增强客户体验&#xff0c;同时提供准确的财务记录。C# 餐饮收银系统是一种使用C#编程语言开发的餐饮业务管理软…

【Java】微服务——Ribbon负载均衡(跟进源码分析原理)

添加LoadBalanced注解&#xff0c;即可实现负载均衡功能&#xff0c;这是什么原理 1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。 2.源码跟踪 为什么我们只输入了service名称就可以访问了呢&#xff1f;之前还要获取…

MySQL - mysql服务基本操作以及基本SQL语句与函数

文章目录 操作mysql客户端与 mysql 服务之间的小九九了解 mysql 基本 SQL 语句语法书写规范SQL分类DDL库表查增 mysql数据类型数值类型字符类型日期类型 示例修改&#xff08;表操作&#xff09; DML添加数据删除数据修改数据 DQL查询多个字段条件查询聚合函数分组查询排序查询…

conda安装使用jupyterlab注意事项

文章目录 一、conda安装1.1 conda安装1.2 常见命令1.3 常见问题 二、jupyterlab2.1 jupyterlab安装和卸载2.2 常见错误2.2.1 版本冲突&#xff0c;jupyterlab无法启动2.2.2 插件版本冲突 2.3 常用插件2.3.1 debugger2.3.2 jupyterlab_code_formatter 2.4 jupyter技巧 一、conda…

iOS---生成证书文件的时候无法选择导出.p12文件

解决办法&#xff1a; 左栏有两个分类&#xff0c;一个钥匙串&#xff0c;一个是种类&#xff0c;要选择种类里面的【我的证书】或【证书】进行导出。选择【系统】找到【我的证书】这样导出不了"个人信息交换(.p12)" 正确做法是&#xff1a;选择【登录】找到【我的…

智能合约漏洞,BEVO 代币损失 4.5 万美元攻击事件分析

智能合约漏洞&#xff0c;BEVO 代币损失 4.5 万美元攻击事件分析 一、事件背景 北京时间 2023 年 1 月 31 日&#xff0c;在 twitter 上看到这样一条消息&#xff1a; BEVO 代币被攻击&#xff0c;总共损失 45000 美元&#xff0c;导致 BEVO 代币的价格下跌了 99%。 有趣的是…

2023蓝帽杯初赛电子取证部分

取证案情介绍&#xff1a; 2021年5月&#xff0c;公安机关侦破了一起投资理财诈骗类案件&#xff0c;受害人陈昊民向公安机关报案称其在微信上认识一名昵称为yang88的网友&#xff0c;在其诱导下通过一款名为维斯塔斯的APP&#xff0c;进行投资理财&#xff0c;被诈骗6万余万元…

CSS 语法

CSS 实例 CSS 规则由两个主要的部分构成&#xff1a;选择器&#xff0c;以及一条或多条声明: 选择器通常是您需要改变样式的 HTML 元素。 每条声明由一个属性和一个值组成。 属性&#xff08;property&#xff09;是您希望设置的样式属性&#xff08;style attribute&#x…