多线程的代码案例

news2024/11/23 9:53:46

目录

单例模式

饿汉模式

懒汉模式

阻塞队列

 生产者消费者模型意义:

阻塞队列使用方法

实现阻塞队列

阻塞队列实现生产者消费者模型

定时器

实现简单的定时器

工厂模式

线程池

为啥呢? 从池子里面取 比 创建线程 效率更高

线程池的创建

怎么填坑

ThreadPoolExecutor

线程数目设置

 实现线程池

 小结


两个设计模式: 单例模式, 工厂模式

单例模式

有些场景中希望有些类仅仅创建一个对象, 代码中很多管理数据的对象都是单例的, MySQL JDBC等.

人可能会出错, 需要编译器帮我们做出监督. 就比如 @Override 必须是方法重写.,在语法层面上没有对单例做出支持, 只能通过编程技巧实现

饿汉模式

刚开始就创建了实例举个例子:

//期望这个类能有唯一实例
class Singleton {
    //设置为静态变量在 Singleton 类被加载时会创建实例
    private static Singleton instance = new Singleton();
    //获取实例
    public static Singleton getInstance() {
        return instance;
    }
    //把构造方法设为 私有 , 类外面的代码无法 new 出类对象了.
    private Singleton() {};
}

注意:

1> 在类的内部提供线程的实例

2> 把构造方法设为 private ,避免其他代码创建实例.

懒汉模式

先判断是否需要创建实例举个例子:

//期望这个类能有唯一实例
class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    //获取实例
    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    //把构造方法设为 私有 , 类外面的代码无法 new 出类对象了.
    private SingletonLazy() {};
}

注意:

1> 第一次判断是否为空原因:

因为加锁开销很大, 而且可能涉及到锁冲突, 所以我们增加一次判断, 不为空直接返回 instance

2> 加锁的原因:

在本操作中会出现读取和修改的操作, 会出现两个都判断为空后创建多个实例的情况.

3> 使用 volatile 原因:

指令重排序问题

编译器为了提高效率, 可能调整代码的执行顺序, 但是必须保持代码逻辑不变, 单线程没问题, 但是多线程可能有问题.

new 操作, 可能触发指令重排序

new 操作分为三步:

1. 申请内存空间

2. 在内存空间上构造对象

3. 把内存地址给 instance

可能按照 123, 132顺序执行, 1一定先执行

在多线程下, 假设  t1线程   按照1 3 2 的顺序  执行1  3后, instance非空指向一个没初始化的非法对象, 这时      t2线程   在判断instance 不为空后, 直接返回一个非法对象, 导致出现bug

使用 volatile 保证不会出现指令重排序问题

阻塞队列

多线程代码中比较常用到的一种数据结构

特殊的队列

1> 线程安全

2> 带有阻塞特性

a) 如果队列为空, 继续出队列, 就会发生阻塞, 阻塞到其他线程往队列里添加元素位置为止

b) 如果队列为满, 继续入队列, 也会发生阻塞, 阻塞到其他线程从队列中取走元素位置为止.

意义: 实现 " 生产者消费者模型 " 一种常见的多线程代码编写方式

举个例子: 包饺子

1> 每个人分别负责擀饺子皮和包饺子

2> 当擀饺子皮快了 就会在 放饺子皮的盖帘满的时候停下来等包饺子的

3> 当包饺子快了 就会停下来等 擀饺子皮的

盖帘就相当于阻塞队列

生产者 把生产出来的内容放到阻塞队列中

消费者 从阻塞队列中获取元素

 生产者消费者模型意义:

1> 解耦合

两个模块联系越紧密, 耦合就越高, 这个模型让耦合降低

2> 削峰填谷

服务器 A 给服务器 B发起请求, 不同服务器消耗的硬件资源不一样, A收到的请求发给B可能就挂了.使用削峰填谷让 B 接受的请求按照 B 的原有节奏处理情况.(这种情况一般不会持续存在, 就好比学校抢课的情况), 峰值过后 B把积压的数据处理掉

阻塞队列使用方法

在 Java 标准库里, 已经提供了现成的 阻塞队列直接使用

在标准库里, 针对 BlockingQueue 提供了两种最重要的实现方式

1> 基于数组

2> 基于链表

BlockingQueue 一般不适用 Queue 中的一些方法, 因为他们不具备阻塞的特性. 

一般使用 (put 阻塞式的入队列), (take 阻塞式的出队列)

示例: 

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        queue.put("111");
        queue.put("222");
        queue.put("333");
        queue.put("444");
        String elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
    }
}

最后一次输出时发生了阻塞.

实现阻塞队列

基于普通队列加上阻塞和线程安全

普通队列基于数组 或者 基于链表

基于数组实现队列理解成一个环

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) throws InterruptedException {
        synchronized (this) {
            while(size == data.length) {
                this.wait();
            }
            data[size] = elem;
            tail++;
            if(tail == data.length) {
                tail = 0;
            }
            size++;
            //这个 notify 用来唤醒 take 中的 wait
            this.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while(size == 0) {
                this.wait();
            }
            String ret = data[head];
            head++;
            if(head == data.length) {
                head = 0;
            }
            size--;
            //这个 notify 用来唤醒 put 中的 wait
            this.notify();
            return ret;
        }
    }
}

wait 除了可以用 notify 唤醒, 还可以用 interrupt 唤醒, 直接整个方法结束了, 因为使用了 throws 抛出异常, 这是没有什么事

如果使用 try catch 方式就会出现bug, 让 tail 把指向的元素覆盖掉了, 然后弄丢了一个元素, 而且 size 也会比数组最长长度还大.(此处不理解看http://t.csdnimg.cn/OBwXN -->中断一个线程目录)

所以在wait 返回的时候进一步确认是否当前队列是满的不是, 如果是满的继续进行wait

所以直接使用 while 判定是否是满的.

为了避免内存可见性问题, 把 volatile 加好

阻塞队列实现生产者消费者模型

package Demo2;


import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class MyBlockingQueue {
    private String[] data = new String[1000];
    // 队列的起始位置
    private volatile int head = 0;
    // 队列的结束位置的下一个位置
    private volatile int tail = 0;
    //队列中有效元素的个数
    private volatile int size = 0;
    //提供的方法 入队列 出队列
    public void put(String elem) throws InterruptedException {
        synchronized (this) {
            while(size == data.length) {
                this.wait();
            }
            data[tail] = elem;
            tail++;
            if(tail == data.length) {
                tail = 0;
            }
            size++;
            //这个 notify 用来唤醒 take 中的 wait
            this.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while(size == 0) {
                this.wait();
            }
            String ret = data[head];
            head++;
            if(head == data.length) {
                head = 0;
            }
            size--;
            //这个 notify 用来唤醒 put 中的 wait
            this.notify();
            return ret;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        // 消费者
        Thread t1 = new Thread(() -> {
            while(true) {
                try {
                    String result = queue.take();
                    System.out.println("消费元素: " + result);
                    Thread.sleep(500);
                } 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();
    }
}

定时器

约定一个时间, 时间到达之后执行某个代码逻辑, 在网络通信中很常见

 在 标准库 中有现成定时器的实现

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

使用匿名内部类的写法继承 TimerTask 创建出实例, 目的时重写 run, 描述任务的详细情况

当前代码也是多线程, timer 里面包含一个线程, 下图是运行结果

可以发现整个进程没有结束, 因为 Timer 内部的线程阻止了进程结束.

 Timer 里面可以安排多个任务.

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

实现简单的定时器

1> Timer 中需要有一个线程, 扫描任务是否到时间了, 可以执行了

2> 需要一个数据结构把所有任务保存起来(使用优先级队列) 

3> 创建一个类, 通过类的对象描述一个任务(至少要包含任务内容和时间)

 其中需要记录, 绝对的时间.

import java.awt.*;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

// 通过这个类, 描述一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
    // 执行的任务
    private Runnable runnable;
    // 执行任务的时间
    private long time;
    // 此处的 delay 就是 schedule 方法传入的 "相对时间"
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    @Override
    public int compareTo(MyTimerTask o) {
        // 让队首元素是最小时间的值
        return (int) (this.time - o.time);
        // 让队首元素是最大时间的值
        //return (int) (o.time - this.time);
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }
}

// 自己的定时器
// 添加元素和扫描线程是不同线程操作同一个队列, 需要加锁 <--原因之一
class MyTimer {
    // 使用一个数据结构, 保存所有的任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 使用这个对象作为锁对象
    private Object locker = new Object();
    public void schedule(Runnable runnable, long delay) {
        synchronized(locker) {
            queue.offer(new MyTimerTask(runnable, delay));
            locker.notify();
        }
    }

    // 扫描线程
    public MyTimer() {
        // 创建一个扫描线程
        Thread t = new Thread(() -> {
            // 扫描线程需要不停扫描看是否到达时间
            while (true) {
                try {
                    synchronized (locker) {
                        // 不要使用 if 作为 wait 的判定条件, 应使用while
                        // 使用 while 是为了在唤醒之后 在再次确认一下条件
                        while (queue.isEmpty()) {
                                locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        // 比较一下当前的队首元素是否可以执行了
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 执行任务
                            task.getRunnable().run();
                            //执行完了, 就从队列中删除
                            queue.poll();
                        } else {
                            // 不可执行, 先等着, 等待下一轮的循环判定
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                }catch (InterruptedException e) {
                            e.printStackTrace();
                        }
            }
        });
        t.start();
    }
}

public class Demo2 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        }, 1000);
    }
}

工厂模式

线程池

线程创建/销毁 比 进程快, 但是进一步提高创建/销毁的频率, 线程的开销也不能忽视了

两种提高效率的方法:

1> 协程 (轻量级线程) 

相对于线程, 把系统调度的过程给忽略了,(程序猿手动调度), 当下比较流行(Java 标准库没有协程)

2> 线程池

兜底, 使线程不至于很慢

例子: 我是个妹子, 在谈男朋友, 一段时间后, 我不想和他好了, 就冷暴力然后分手, 分手之后再去找另一个小哥哥, 然后和另一个小哥哥好上了. 

线程池就是我在谈第一个男朋友的时候就同时和其他小哥哥搞暧昧(培养感情), 哪天想分手了直接分, 然后无缝衔接

线程池: 在使用第一个线程的时候, 提前把 2, 3, 4, 5线程创建好(培养感情), 后续想使用新的线程不必创建, 直接使用(创建线程的开销降低了)

为啥呢? 从池子里面取 比 创建线程 效率更高

从池子里取, 就是纯粹用户态操作

创建新的线程需要 用户态 + 内核态 相互配合 完成

操作系统是由 内核 + 配套的应用程序 构成

内核 是系统最核心的部分, 创建线程操作需要调用系统 api, 进入到内核中, 按照内核态的方式来完成一系列动作

当你想要创建线程的时候, 内核需要给所有进程提供服务, 不可控, 难以避免会做一些其他的事导致效率减低

线程池的创建

Java标准库提供了写好的线程池.

创建线程池对象并没有 new , 而是通过专门的方法返回了一个线程池对象(工厂模式), 通常创建对象使用 new , new 就会触发类的构造方法, 但构造方法存在一定的局限性. 工厂模式是给构造方法填坑的.

怎么填坑

我们构造一个对象希望有多种构造方式, 这就需要多个构造方法, 但是构造方法的名字必须是类名, 不同的构造方法只能通过 重载区分, 但是如果实现方法不一样, 但是参数类型/个数一样咋办呢?

使用工厂设计模式, 使用普通的方法代替构造方法完成初始化工作, 普通方法使用名字区分.

 Executors 是一个 工厂类, newCachedThreadPool 是工厂方法, 使用静态方法通过类名调用

工厂方法有很多, 上述方法创建出来的线程池对象的线程数目可以动态适应, 随着王线程池里面添加任务, 线程池中的线程自动创建, 创建出来在池子里保留一定时间以备后续使用.

这个方法是固定的线程池, 调用方法时手动指定创建几个线程

 还用很多其他线程池上面介绍的两种用的更多一点

ThreadPoolExecutor

上述工厂方法生成的线程池本质上是对 类(ThreadPoolExecutor) 的封装

核心方法:

1> 添加任务

2> 构造

举例:  1> 添加任务 (简单) 

使用 submit 把任务交给线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo3 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

 2> 构造方法 (重点)

构造方法中参数很多[经典面试题]

在 juc 包里面, 并发编程相关内容 

全部参数 如下图:

对这 4 种情况 举个例子:

我有 任务 A 要做, 朋友来让我帮忙做任务 B, 这时我有 4 种回应方法.

1> 我心态崩了, 大哭. 抛出异常

2> 我对朋友说你自己做, 朋友自己做任务 B

3> 我的任务 A 不做了, 就去帮朋友

4> 我直接拒绝帮忙, 我仍然做任务 A , 朋友也不做任务 B 了

线程数目设置

使用线程池需要设置线程的数目, 设置多少合适?

具体数目是不对的, 需要实际情况分析

原因:

一个线程执行代码主要有两类:

1> cpu 密集型: 代码主要是进行 算术运算/逻辑判断

2> IO密集型: 代码里主要进行的是 IO 操作

如果是 1>  这个时候线程池的数量不要超过 N (设 N 就是极限), 比 N 更大, 就无法提高效率了, cpu吃满了, 线程越多反而增加调度的开销

如果是 2>  不吃 CPU, 此时设置的线程数可以超过 N, 一个核心可以通过调度的方式来并发执行.

 实现线程池

class MyThreaPool {
    // 任务队列
    private BlockingDeque<Runnable> queue = new ArrayBlockingQueue<>();
    // 通过这个方法, 把任务添加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        //此处策略是第 5 种, 拒绝策略, 阻塞等待
        queue.offer(runnable);
    }
    public MyThreaPool(int n) {
        // 创建出 n 个线程, 负责执行上述队列中的任务
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                // 让这个线程从队列中消费任务,并进行执行
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}

 小结

认真学习各种多线程代码实例, 理解其中的含义, 将各个代码的的易错点分析透彻

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

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

相关文章

[HNCTF 2024] crypto/pwn

周日的比赛&#xff0c;赛后拿别人的WP又作了俩&#xff0c;最后一个题也是没弄懂&#xff0c;先记一下吧。 Crypto EZmath 一个简单的函数题。在sagemath里有个two_squares函数&#xff0c;可以从平方和恢复两个规模相近的数。这种比较适合于RSA里的p,q。另外未知的e用来猜…

TortoiseGit的安装

TortoiseSvn和TortoiseGit都是针对代码进行版本管理的工具&#xff0c;又俗称小乌龟&#xff0c;简洁而可视化的操作界面&#xff0c;免去繁琐的命令行输入。只需要记住常用的几个操作步骤就能快速上手。 TortoiseGit安装 1、TortoiseGit作为git的版本管理工具 &#xff0c;但…

小程序蓝牙连接ESP32通信(可直接拿来用)

小程序中的蓝牙能力 在小程序中&#xff0c;要使用蓝牙能力&#xff08;Beacon 除外&#xff09;必须首先调用 wx.openBluetoothAdapter 初始化蓝牙适配器模块&#xff0c;其生效周期为调用 wx.openBluetoothAdapter 至调用 wx.closeBluetoothAdapter 或小程序被销毁为止。只有…

红黑树底层封装map、set C++

目录 一、框架思考 三个问题 问题1的解决 问题2的解决&#xff1a; 问题3的解决&#xff1a; 二、泛型编程 1、仿函数的泛型编程 2、迭代器的泛型编程 3、typename&#xff1a; 4、/--重载 三、原码 红黑树 map set 一、框架思考 map和set都是使用红黑树底层&…

【STM32-MX_GPIO_Init分析】

MX_GPIO_Init分析源码如下&#xff1a; __HAL_RCC_GPIOE_CLK_ENABLE源码如下&#xff1a; #define RCC ((RCC_TypeDef *) RCC_BASE) #define RCC_BASE (AHB1PERIPH_BASE 0x3800UL) #define AHB1PERIPH_BASE (PERIPH_BASE 0x00020000U…

2024-简单点-ultralytics库解析-data模块

data模块 overview布局\_\_init__.pyfrom .base import BaseDataset\_\_all__ annotator.pyaugment.pyclass BaseTransformclass Composeclass BaseMixTransformclass Mosaic静态方法更新label class MixUpRandomPerspectiveclass RandomHSVclass RandomFlipclass LetterBoxcla…

搭载全新升级viaim AI,讯飞会议耳机Pro 2首销价1399元起

2024年5月15日&#xff0c;人工智能硬件公司未来智能发布了讯飞会议耳机Pro 2、iFLYBUDS 2以及Kit 2三款旗舰新品&#xff0c;为用户带来全新升级的viaim AI&#xff0c;也为AIGC智能耳机树立了新标杆。 在发布会上&#xff0c;未来智能CEO马啸表示&#xff1a;在AIGC领域&…

20232803 2023-2024-2 《网络攻防实践》实践九报告

目录 1.实践内容2.实践过程2.1 手工修改可执行文件&#xff0c;改变程序执行流程&#xff0c;直接跳转到getShell函数2.2 利用foo函数的Bof漏洞&#xff0c;构造一个攻击输入字符串&#xff0c;覆盖返回地址&#xff0c;触发getShell函数2.3 注入一个自己制作的shellcode并运行…

数论专题练习

质数专题 我的思路就是一个素数筛&#xff0c;然后双指针 class Solution { public:int maximumPrimeDifference(vector<int>& nums) {unordered_map<int, int> mp;for (int i 2; i < 100; i) {if (mp[i] 0) {for (int j 2 * i; j < 100; j i) {mp[…

将PDF转换成电子杂志,轻松打造畅销内容!

在数字化时代&#xff0c;将PDF转换成电子杂志是一种非常受欢迎的内容创作方式。这种方式不仅可以提高内容的传播效果&#xff0c;还可以为创作者带来更多的收益。那么&#xff0c;如何轻松地将PDF转换成电子杂志&#xff0c;打造畅销内容呢&#xff1f; 市面上有许多可以将PDF…

vivo X100s发布,搭载最新天玑9300+平台

在沉寂了半年后&#xff0c;vivo终于发布了新的旗舰产品。相较于前代的X100&#xff0c;X100s作为小迭代也有不少让人眼前一亮的地方&#xff0c;下面就让我们一同来了解下吧。 外观方面&#xff0c;虽然vivo X100s相较于X100没有大改&#xff0c;但却十分具有质感。以“青云”…

Android 逆向

一、apk 查壳工具 ApkScan-PKID 相关APK文件可以在 豌豆荚 官网下载 ApkScan-PKID查壳工具 下载 - 简书 (jianshu.com) 二、脱壳工具&#xff1a;frida 1、Android端配置 frida-server&#xff1a; 该步骤需要使用到 adb&#xff0c;操作Android文件 Releases frida/frid…

机器学习中10种损失函数大梳理!建议收藏,你一定用得到

今儿想和大家聊聊关于损失函数方面的问题。 损失函数&#xff08;Loss Function&#xff09;是在机器学习和深度学习中用来衡量模型预测值与真实标签之间差异的函数。不同的任务和模型可能需要不同的损失函数。 今天就聊聊下面常见的损失函数&#xff0c;关于原理、使用场景&…

高效调度新篇章:详解DolphinScheduler 3.2.0生产级集群搭建

转载自tuoluzhe8521 导读&#xff1a;通过简化复杂的任务依赖关系&#xff0c; DolphinScheduler为数据工程师提供了强大的工作流程管理和调度能力。在3.2.0版本中&#xff0c;DolphinScheduler带来了一系列新功能和改进&#xff0c;使其在生产环境中的稳定性和可用性得到了显著…

Apache2.4和PHP8的量子纠缠

Apache不建议你用&#xff0c;PHP建议使用

更新Windows 11 后遇到的一些问题(更新中...)

目录 插入U盘后读取不到 在磁盘中新建文件夹需要管理员权限 导致不能安装一些软件 插入U盘后读取不到 解决方法&#xff1a;点击我的电脑或者是此电脑、选择管理、找到设备管理器、选择通用串行总线控制器、右键、选择启动。 第一步&#xff1a;点击我的电脑或者是此电脑、选…

Java类和对象(二)—— 封装,static 关键字与代码块

前言 在面向对象的编程语言中&#xff0c;有三大特性&#xff1a;封装、继承和多态~~ 今天我们就来学习封装的知识 封装 什么是封装 在现实生活中&#xff0c;我们经常使用手机来进行沟通与交流&#xff0c;实际上我们拿到的手机是被封装好的&#xff0c;精美的屏幕&a…

MYSQL和JAVA中将中文汉字按照拼音首字母排序

一、MYSQL将中文汉字按照拼音首字母排序 数据库使用的字符编码是utf8_general_ci&#xff0c;如下 ORDER BY CONVERT(表名.字段名 USING gbk) COLLATE gbk_chinese_ci ASC;若是表查询&#xff0c;CONVERT中可以不添加表名。 查询结果如下&#xff1a; 二、JAVA中将中文汉字…

自定义 Gradle 插件进行统一的静态代码分析

静态代码分析是一项了不起的技术, 它能让代码库更易于维护. 但是, 如果你在不同的版本库中拥有多个服务(可能由不同的团队开发), 如何才能让每个人都遵循既定的代码风格呢? 一个好办法是将所有规则封装在一个插件中, 该插件会在每个项目构建时自动执行所需的验证. 因此, 在本…

【2024系统架构设计】回顾历史,查缺补漏篇 ③

前言 hello,大家好: 💡💡💡 我们一起来备考软考高级系统架构设计师吧,本专栏提供综合知识、案例科目、论文(论点和部分示例范文)等内容,包括知识点总结和记忆小妙招哦。 🚀🚀🚀 可以减少资料查找和收集的时间,提高效率,我们一起集中精力学习干货吧! 💡…