多线程相关案例

news2024/12/26 19:52:45

目录

1. 单例模式

1) 饿汉模式

2) 懒汉模式

2. 阻塞队列

1) 阻塞队列的特性

2) 模拟实现阻塞队列

3. 定时器

4. 线程池

1) ThreadPoolExecutor 类

2) 模拟实现线程池


1. 单例模式

单例模式是最经典的设计模式之一。

单例模式,顾名思义,就是这个类在整个程序中只能有一个实例。

具体来说就是,约定一个类,只能有一个对象,通过编码技巧,让编译器强制进行检查,提前在类里把对象给创建好,然后将构造方法设为 private。

单例模式有两种实现方式,一种是饿汉模式,一种是懒汉模式。

懒就是,非必要,不采取行动,举个例子,在早上你可以一直睡觉,直到上学快要迟到了,你再起床,这样做的话,就能休息时间最大化,保证今天的学习效率。

而饿的话,就是截然相反,早上就非常早起床,然后马上去上学,起太早的话,可能就会睡眠不足,导致上课的时候昏昏欲睡,学习效率低。

1) 饿汉模式

我们可以具体来实现一下单例模式,用饿汉模式的方式,这个比较简单。

因为是单例模式嘛,而考虑到是饿汉的方式来实现,我们就可以不管三七二十一,先提前创建好对象,等到有人想要获取对象的时候,就直接返回创建好的对象,但是只做这些还不足以让它成为单例模式,我们还需要将它的构造方法设置成私有的,并且要将获取对象的方法设置成 public,并且用 static 修饰,这样就可以通过 类名.方法名 来调用获取实例的方法了。

最后写出来就是这个样子的:

// 期望这个类,能够有唯一实例
class Singleton {
    private static final Singleton instance = new Singleton();

    // 通过这个方法获取到刚才的实例
    // 后续如果想使用这个类的实例,就都通过 getInstance 方法来获取
    public static Singleton getInstance() {
        return instance;
    }

    // 把构造方法设为 private,此时类外面的其他代码,就无法 new 出这个类的对象了
    private Singleton() {
    }
}

2) 懒汉模式

接下来再来看看单例模式如何用懒汉的方式来实现。

懒,就是非必要,不采取行动。

那我们就可以先不初始化对象,等到有人调用获取对象的方法的时候,再来创建对象,然后再返回创建好的对象,那整个代码就跟饿汉方式差不多,就是延迟了创建对象的时机。

写出来就是这样的。 

这下修改完代码后,就才真正没问题了。

class SingletonLazy {
    private static volatile SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        // 是否是首次创建对象(从而判断要不要加锁)
        if (instance == null) {
            // 保证串行化执行,不会 new 多个对象
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {

    }
}

饿汉方式实现的单例模式会涉及到线程安全的问题,需要注意三点:

1. 加锁,保证 if 条件和 new 操作是原子的,只会创建出一个对象。

2. 两层 if,第一个 if 用来判断是否要加锁,第二个 if 用来判断是否要创建对象。

3. volatile,用来禁止指令重排序问题。

而饿汉模式天然就是线程安全的,因为它只涉及到了多个线程的读取操作。

2. 阻塞队列

阻塞队列是多线程代码中常用到的一种数据结构。

1) 阻塞队列的特性

阻塞队列的特性有两个:

1. 线程安全

2. 带有阻塞特性

     a) 当队列为空时,从队列中取元素的话,就会阻塞,阻塞到其他线程往队列里添加元素为止

     b) 当队列为满时,往队列中放元素的话,就会阻塞,阻塞到其他线程从队列中取走元素为止

阻塞队列最大的意义,就是用来实现 "生产者消费者模型"(是一种常见的编码方式)。

怎么理解生产者消费者模型呢?举个例子:

那么为什么要使用生产者消费者模型呢?生产者消费者模型的优点是什么?

生产者消费者模型的优点:

1. 解耦合

两个模块,联系越紧密,耦合就越高。

2. 削峰填谷

峰:短时间内请求量比较多

谷:请求量比较少

在 Java 标准库里,提供了现成的阻塞队列 BlockingQueue,来让我们使用。

标准库里,针对于阻塞队列提供了两种最主要的实现方式: 1. 基于链表  2. 基于数组

虽然 BlockingQueue 继承了 Queue,但是不建议在 BlockingQueue 里使用 Queue 的方法,因为这些方法都不具备 "阻塞" 特性。

put 方法是阻塞式的入队列,take 方法是阻塞式的出队列。

2) 模拟实现阻塞队列

了解完上面这些,我们自己也可以模拟实现一个阻塞队列,阻塞队列就是比普通队列多了线程安全以及阻塞特性,那我们就可以先实现一个队列(只用实现 put 和 take 方法),然后再加上线程安全(synchronized),再加上阻塞就好了(wait & notify)。

首先,普通队列有两种实现方式,一种是基于数组,另一种是基于链表。

基于链表实现的话,就是头插尾删,蛮简单的,

基于数组实现的话,就是要实现一个循环数组。

这里我们使用数组来实现好了,添加元素就是往 tail 下标处添加元素,删除元素就是删除 head 处元素,直接 head++ 即可。

有了上面基础,我们就可以直接开始敲代码了,只用实现 put 和 take 方法就行。

首先搭好框架,然后再来开始写代码。

// 不写作泛型了,直接让这个队列里面存储字符串
class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,也可以构造方法的参数来判定
    private String[] data = new String[1000];

    private int head = 0;// 队列的起始位置
    private int tail = 0;// 队列的结束位置的下一个位置
    private int size = 0;// 记录当前队列有效元素个数

    // 提供核心方法,入队列和出队列
    public void put(String elem) {
        
    }

    public String take() {
        
    }
}

大致写好了普通队列,接下来再保证线程安全,首先是涉及到要修改变量的操作,就得加锁,因为两个方法大部分都有修改操作,那我们就可以直接创建个 Object 对象,然后加锁把方法包裹起来。

然后也要注意内存可见性和指令重排序的问题,以防万一,给那几个变量加上 volatile 会比较好。

最后再加上阻塞,使用 wait & notify 来完成就好。

官方文档建议,在使用 wait 的时候,最好搭配 while 来使用(将 if 换成 while)

完整代码:

// 不写作泛型了,直接让这个队列里面存储字符串
class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,也可以构造方法的参数来判定
    private String[] data = new String[1000];

    private volatile int head = 0;// 队列的起始位置
    private volatile int tail = 0;// 队列的结束位置的下一个位置
    private volatile int size = 0;// 记录当前队列有效元素个数
    private Object locker = new Object();

    // 提供核心方法,入队列和出队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            // 首先判断,队列满不满
            while (size == data.length) {
                // 阻塞, 等到有其他线程取元素的时候,再唤醒
                locker.wait();
            }
            // 队列没满,就真正的往里面放元素
            data[tail] = elem;
            tail = (tail + 1) % data.length;
            size++;
            locker.notify();// 这个 notify 用来唤醒 take 的 wait
        }

    }

    public String take() throws InterruptedException {
        synchronized (locker) {
            while (size == 0) {
                // 队列为空,阻塞,等到后面有其他线程添加元素后再唤醒
                locker.wait();
            }
            // 队列不空,就需要把 head 位置的元素给删除掉,并且返回
            String ret = data[head];
            head = (head + 1) % data.length;
            size--;
            // 这个 notify 用来唤醒 put 的 wait
            locker.notify();
            return ret;
        }
    }
}

借助这个阻塞队列,我们就可以实现一个简单的生产者消费者模型,就是一个线程往里面添加元素,另一个线程从里面消费元素。

// 不写作泛型了,直接让这个队列里面存储字符串
class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,也可以构造方法的参数来判定
    private String[] data = new String[1000];

    private volatile int head = 0;// 队列的起始位置
    private volatile int tail = 0;// 队列的结束位置的下一个位置
    private volatile int size = 0;// 记录当前队列有效元素个数
    private Object locker = new Object();

    // 提供核心方法,入队列和出队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            // 首先判断,队列满不满
            while (size == data.length) {
                // 阻塞, 等到有其他线程取元素的时候,再唤醒
                locker.wait();
            }
            data[tail] = elem;
            tail = (tail + 1) % data.length;
            size++;
            locker.notify();
        }

    }

    public String take() throws InterruptedException {
        synchronized (locker) {
            while (size == 0) {
                // 队列为空,阻塞,等到后面有其他线程添加元素后再唤醒
                locker.wait();
            }
            String ret = data[head];
            head = (head + 1) % data.length;
            size--;
            locker.notify();
            return ret;
        }
    }
}


public class Demo24 {
    public static void main(String[] args) {
        // 生产者,消费者,分别使用一个线程表示。(也可以使用多个线程)
        MyBlockingQueue queue = new MyBlockingQueue();

        // 消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    String num = queue.take();
                    System.out.println("消费元素:" + num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });
        // 生产者
        Thread t2 = new Thread(() -> {
            int num = 1;
            while (true) {
                try {
                    queue.put(num + "");
                    System.out.println("生产元素:" + num);
                    num++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果,发现生产者一下子把队列生产满了,后面就是消费者消费一个元素,生产者生产一个元素。

3. 定时器

定时器是一个日常开发的常见组件。

约定一个时间,等时间到了之后,就会执行某个代码逻辑。

这个就跟闹钟差不多。

定时器非常常见,尤其是在进行网络通信的时候。

举个例子:

而 Java 标准库也给我们提供现成的定时器 Timer 类,来让我们使用。

// 定时器
public class Demo25 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 给定时器安排了一个任务,预定在 xxx 时间去执行。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        }, 1000);
        System.out.println("程序启动!");
    }
}

了解上述内容后,我们自己也可以模拟实现一个定时器。

刚刚也说了,定时器内部包含了一个扫描线程,以及一个任务对象的类,来专门表示任务和时间。

那我们也可以根据这两点来实现定时器:

1. 定时器内部需要有一个线程,负责扫描时间

2. 定时器可以添加多个任务,所以需要用一个数据结构来存放任务对象

3. 需要创建一个类,来描述任务(必须包括时间和任务)

那该用哪个数据结构来组织任务对象呢?

定时器跟时间有关,并且时间短的任务先执行,那我们就可以使用优先级队列来组织任务对象。

根据这些,我们就可以敲代码了,首先创建出两个类 MyTimer 和 MyTimerTask,分别用来表示定时器和任务,MyTimer 里需要存放任务列表,所以还需要添加一个优先级队列 queue(记得要传比较器),而 MyTimerTask 里包含了时间和任务,并为它们提供构造方法,MyTimer 构造方法就需要创建出一个线程,用来扫描任务,还有一个 schedule 方法,用来添加任务。

// 模拟实现一个简单的定时器
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
        @Override
        public int compare(MyTimerTask o1, MyTimerTask o2) {
            // 时间小的先执行,建立小根堆
            return (int) (o1.getTime() - o2.getTime());
        }
    });

    public MyTimer() {
        Thread thread = new Thread(() -> {
            // 写扫描线程的逻辑
        });
        // 不要忘记启动线程
        thread.start();
    }

    public void schedule(MyTimerTask task) {
        queue.offer(task);
    }
}

class MyTimerTask {
    private long time;
    private Runnable runnable;

    public MyTimerTask(long time, Runnable runnable) {
        // 这里记录绝对时间方便我们计算
        this.time = time + System.currentTimeMillis();
        this.runnable = runnable;
    }

    // 提供 getter 方法
    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}

然后再来实现扫描线程的工作。

这个其实也很简单,

先判断队列是否为空,如果队列为空,就需要阻塞等待,直到有线程调用 schedule 方法,

不空的话,只要不停地看一下任务队列里的队首元素,看一下该元素的时间到没到,到了的话就执行任务,执行完任务后将任务从队列里弹出,

没到的话,就等时间到了再来执行任务,(也可以看做是懒,非必要,不采取行动)

而前面也说了,要让线程阻塞等待,那就可以使用 wait,而使用 wait 就必须得先加锁,此时,我们发现其实要进行阻塞的这几处,都有修改操作,schedule 是要往队列里面添加新任务,而扫描线程扫描任务列表,当时间到了,就得执行任务,执行完任务后就得将任务出队,那这样的话,我们加锁和使用 wait 就是顺理成章的事情了。

写完后就是这样的:

// 模拟实现一个简单的定时器
class MyTimer {
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {
        @Override
        public int compare(MyTimerTask o1, MyTimerTask o2) {
            // 时间小的先执行,建立小根堆
            return (int) (o1.getTime() - o2.getTime());
        }
    });

    public MyTimer() {
        Thread thread = new Thread(() -> {
            synchronized (this) {
                // 写扫描线程的逻辑
                while (true) {
                    // 首先判断队列为不为空,空的话,就阻塞等待
                    // 直到有线程调用 schedule 方法为止
                    while (queue.isEmpty()) {
                        // 要加锁
                        // 阻塞等待
                        try {
                            this.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask task = queue.peek();
                    if (System.currentTimeMillis() >= task.getTime()) {
                        // 时间到了,需要执行任务, 然后出队列
                        task.getRunnable().run();
                        queue.poll();
                    } else {
                        // 没到时间的话,就进行等待
                        try {
                            // 如果添加了新的任务,也需要将线程唤醒
                            // 重新更新一下,最早的任务是什么,以及更新等待时间
                            this.wait(task.getTime() - System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        // 最后要记得启动线程
        thread.start();
    }

    public void schedule(Runnable runnable, long time) {
        synchronized (this) {
            queue.offer(new MyTimerTask(time, runnable));
            this.notify();
        }
    }
}

class MyTimerTask {
    private long time;
    private Runnable runnable;

    public MyTimerTask(long time, Runnable runnable) {
        // 这里记录绝对时间方便我们计算
        this.time = time + System.currentTimeMillis();
        this.runnable = runnable;
    }

    // 提供 getter 方法
    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}

写完之后,我们就可以来测试一下,看看代码是否正确,就写个例子,调用下方法就行。

没什么问题,一个简单的定时器就写好了

4. 线程池

线程诞生的意义,是因为进程太重量。

而线程因为创建/销毁的时候不需要申请/释放资源,所以比进程快,线程也叫做轻量级进程。

但是,如果频繁的创建销毁线程,此时这个开销也是不容忽视的。

此时,就有两种提高效率的方法,第一种:协程。第二种:线程池。

协程也被叫做轻量级线程,它相比于线程,把系统调度的过程,给省去了,就是让我们自己来调度

但是很遗憾,在 Java 协程的圈子里,很少有人会用协程。

主要有两个原因:第一,Java 官方没有实现协程,虽然有第三方库实现了,但是不够权威也不够靠谱。    第二,Java 有线程池,有线程池兜着底,让线程也不至于太慢。

那到底什么是线程池呢?

可以先看看,这里面的池是什么意思。

池其实是计算机中一种重要的思想方法,很多地方都会涉及到(如线程池、进程池、内存池、连接池)。

举个例子来说明吧:

那么线程池,就是在使用第一个线程的时候,提前把线程 2、3、4、5....给创建出来,

如果后续想要使用新的线程,不用重新创建,而是直接从线程池里面拿就好,就能降低创建线程的开销。

Java 标准库提供了写好的线程池,来让我们使用。

1) ThreadPoolExecutor 类

ThreadPoolExecutor 类的功能非常丰富,提供了很多参数,上述标准库的几个方法,就是给这个类填写了不同的参数用来构造线程池。

我们也可以来学学这个类(面试会考)。

ThreadPoolExecutor 的核心方法就两个:1. 构造(构造方法参数很多)      2. 注册任务(添加任务)

我们来看看它的构造方法,直接看最下面的构造方法就行,因为这个的参数涵盖了上面的参数。

(有一说一,第一次看到这么多参数的构造方法,天都要塌了,但是理解之后其实还好)

2) 模拟实现线程池

了解以上这些后,我们可以自己来实现一个线程池。

线程池:写一个固定线程数目的线程池(暂时不考虑线程的增加和减少)

(1) 提供构造方法,指定创建多少个线程

(2) 在构造方法中,把这些线程都创建好

(3) 有一个阻塞队列,能够用来存放要执行的任务

(4) 提供 submit 方法,用来添加任务

有了以上思路,就很好写代码了。

// 模拟实现一个简单的线程池
class MyThreadPool {
    
    // 将创建好的线程放在数组里面,等到需要使用的时候就拿出来用
    private List<Thread> threadList = new ArrayList<>();
    // 拒绝策略就是阻塞等待,直到别的线程使用 submit 方法添加任务为止
    BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 任务队列

    // 通过这个方法,把任务添加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        // 此处我们的拒绝策略,相当于第五种拒绝策略了,阻塞等待(这是下策)
        queue.put(runnable);
    }

    public MyThreadPool(int n) {
        // 创建出 n 个线程,负责执行上述队列中的任务
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                // 让每个线程不停的从队列中消费任务,如果没有任务了,
                // 那此时线程就会阻塞等待,直到有其他线程调用 submit 方法为止
                while (true) {
                    // 让这个线程,从队列中消费任务,并进行执行
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
            threadList.add(t);
        }
    }
}

写个简单的代码来测试一下:

没啥问题。

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

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

相关文章

【PyQt入门】麦当劳会员登录页面实战

PyQt思维导图&#xff1a; 效果图如下&#xff1a; 设计页面包含&#xff1a;图标&#xff08;含动图gif&#xff09;&#xff0c;窗口logo&#xff0c;title&#xff0c;文本框&#xff0c;按钮 素材图如下&#xff1a; 完整代码以及标注如下&#xff1a; # 导入必要的PyQt6…

中断,定时器相关内容

中断&#xff0c;定时器相关内容 单片机中断什么是单片机的中断中断嵌套中断的优点中断结构中断相关寄存器中断优先级IP中断号中断响应条件中断使用实例在这里插入代码片 定时器CPU 时序的有关知识定时器原理定时计数结构定时/计数器的寄存器定时器配置功能实现 单片机中断 高位…

五层网络协议(封装和分用)

目录 七层网络协议五层网络协议封装1.应用层2.传输层3.网络层4.数据链路层5.物理层 分用1. 物理层2.数据链路层3.网络层 IP 协议4.传输层 UDP 协议5.应用层 七层网络协议 网络通信过程中&#xff0c;需要涉及到的细节&#xff0c;其实是非常非常多的&#xff0c;如果要有一个协…

在鲲鹏麒麟服务器上部署MySQL主从集群

因项目需求需要部署主从MySQL集群&#xff0c;继续采用上次的部署的MySQL镜像arm64v8/mysql:latest&#xff0c;版本信息为v8.1.0。计划部署服务器192.168.31.100和192.168.31.101 部署MySQL主节点 在192.168.31.100上先创建好/data/docker/mysql/data和/data/docker/mysql/l…

一款支持80+语言,包括:拉丁文、中文、阿拉伯文、梵文等开源OCR库

大家好&#xff0c;今天给大家分享一个基于PyTorch的OCR库EasyOCR&#xff0c;它允许开发者通过简单的API调用来读取图片中的文本&#xff0c;无需复杂的模型训练过程。 项目介绍 EasyOCR 是一个基于Python的开源项目&#xff0c;它提供了一个简单易用的光学字符识别&#xff…

C++学习日记---第16天

笔记复习 1.C对象模型 在C中&#xff0c;类内的成员变量和成员函数分开存储 我们知道&#xff0c;C中的成员变量和成员函数均可分为两种&#xff0c;一种是普通的&#xff0c;一种是静态的&#xff0c;对于静态成员变量和静态成员函数&#xff0c;我们知道他们不属于类的对象…

如何搭建JMeter分布式集群环境来进行性能测试

在性能测试中&#xff0c;当面对海量用户请求的压力测试时&#xff0c;单机模式的JMeter往往力不从心。如何通过分布式集群环境&#xff0c;充分发挥JMeter的性能测试能力&#xff1f;这正是许多测试工程师在面临高并发、海量数据时最关注的问题。那么&#xff0c;如何轻松搭建…

人工智能-卷积神经网络(学习向)

一.概述&#xff1b; 卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是一种专门用于处理具有类似网格结构的数据&#xff08;如图像&#xff09;的深度学习模型。 主要用于处理机器视觉任务。 主要功能&#xff1b; 1.图像分类 2.目标检测 3.图像分割…

一些基于宏基因组的巨型病毒研究

Introduction 上次已经介绍了巨型病毒的一些基本内容&#xff0c;也讲到了不依赖培养的方法是从环境样本中发现巨型病毒基因组成的不可或缺的工具。可以通过基因组解析宏基因组学来从环境序列数据中获取 NCLDV 基因组并进行深入研究如功能基因&#xff0c;宿主&#xff0c;进化…

【Django-xadmin】

时间长不用,会忘的系列 1、Django-xadmin后台字段显示处理 主要是修改每个模块下adminx.py文件 代码解释&#xff1a;第1行控制表单字段显示第2行控制列表字段显示第3行控制搜索条件第4行控制过滤条件第5行支持单个或多个字段信息修改第6行列表分页&#xff0c;每页显示多少行…

深入浅出体验AI生图产品Dall-E

DALL-E是由OpenAI开发的一种革命性的AI图像生成工具&#xff0c;能够根据文本描述生成图像。它的名字灵感来源于著名画家萨尔瓦多达利&#xff08;Salvador Dal&#xff09;和皮克斯动画电影中的角色瓦力&#xff08;WALL-E&#xff09;&#xff0c;这暗示了其在艺术创造力与技…

域名解析系统 DNS

1.域名系统概述 用户与互联网上某台主机通信时&#xff0c;必须要知道对方的IP地址。然而用户很难记住长达32 位的二进制主机地址。即使是点分十进制地址也并不太容易记忆。但在应用层为了便于用户记忆各种网络应用&#xff0c;连接在互联网上的主机不仅有P地址&#xff0c;而…

学习ASP.NET Core的身份认证(基于Session的身份认证3)

开源博客项目Blog中提供了另一种访问控制方式&#xff0c;其基于自定义类及函数的特性类控制访问权限。本文学习并测试开源博客项目Blog的访问控制方式&#xff0c;测试程序中直接复用开源博客项目Blog中的相关类及接口定义&#xff0c;并在其上调整判断逻辑。   首先是接口A…

十六(AJAX3)、XMLHttpRequest、Promise、简易axios封装、案例天气预报、lodash-debounce防抖

1. XMLHttpRequest 1.1 XMLHttpRequest-基本使用 /* 定义&#xff1a;XMLHttpRequest&#xff08;XHR&#xff09;对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL&#xff0c;获取数据。这允许网页在不影响用户操作的情况下&#xff0c;更…

【QT】音乐播放器demo

1、使用设计师模式绘制ui界面 添加QPushButton并设置大小&#xff0c;ctrl鼠标拖动复制相同的组件。 添加icon //ps:icon下载网站 设置按钮无边框并设置鼠标悬停颜色&#xff1a; 修改QWidget样式表&#xff0c;添加&#xff1a; *{ border:none; } QPushBu…

「Mac畅玩鸿蒙与硬件34」UI互动应用篇11 - 颜色选择器

本篇将带你实现一个颜色选择器应用。用户可以从预设颜色中选择&#xff0c;或者通过输入颜色代码自定义颜色来动态更改界面背景。该应用展示了如何结合用户输入、状态管理和界面动态更新的功能。 关键词 UI互动应用颜色选择器状态管理用户输入界面动态更新 一、功能说明 颜色…

T620存储安全方案SoC芯片技术手册

系统资源 集成32位国产CPU CK803S&#xff1b;最高工作频率260Mhz CK803S内置16KB I/D Cache&#xff0c;内置32KB DTCM 32KB ROM&#xff1b;256KB SRAM&#xff1b;8KB SRAM&#xff08;系统专用&#xff09; 512KB/1MB 片内Flash 安全算法 支持SM4数据加密&#xff0c;加密性…

计算机光电成像理论基础

一、透过散射介质成像 1.1 光在散射介质中传输 光子携带物体信息并进行成像的过程是一个涉及光与物质相互作用的物理现象。这个过程可以分为几个步骤来理解&#xff1a; 1. **光的发射或反射**&#xff1a; - 自然界中的物体可以发射光&#xff08;如太阳&#xff09;&am…

C语言——自我介绍_Gitee的基本使用

自我介绍 一名信息安全技术应用专业的大学生&#xff0c;来到CSDN博客论坛已有两年。写博客的目的&#xff1a;第一点是为了学习到更多的知识&#xff0c;以便以后所需&#xff1b;第二点是为了读者&#xff0c;俺是一个初学者&#xff0c;希望可以和读者朋友共同进步&#xf…

Redis高阶集群搭建+集群读写

问题 容量不够&#xff0c;redis 如何进行扩容&#xff1f;并发写操作&#xff0c; redis 如何分摊&#xff1f;另外&#xff0c;主从模式&#xff0c;薪火相传模式&#xff0c;主机宕机&#xff0c;导致 ip 地址发生变化&#xff0c;应用程序中配置需要修改对应的主机地址、端…