【JavaEE初阶】多线程进阶(六)JUC 线程安全的集合类

news2024/11/20 18:27:47

在这里插入图片描述

文章目录

  • JUC(java.util.concurrent)的常见类
    • Callable接口
      • 相关面试题
    • ReentrantLock(可重入锁)
    • 原子类
    • 信号量Semaphore
    • CountDownLatch
  • 线程安全的集合类
    • 多线程环境使用 ArrayList
    • 多线程使用队列
    • 多线程使用哈希表(重点)
      • 相关面试题

JUC(java.util.concurrent)的常见类

Callable接口

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.

常见的创建线程的方式有两种方式, 第一种方法是直接继承Thread类, 重写run方法, 第二种方法是实现Runnable接口, 然后还是要靠Thread类的构造器, 把Runnable传进去, 最终调用的就是Runnablerun方法。; 和Runnable类似, 我们还可以通过Callable接口描述一个任务配合FutureTask类来创建线程, 和Runnable不同的是,Callable接口配合FutureTask类所创建的线程其中的任务是可以带有返回值的, 而一开始提到的那两种方式任务是不支持带返回值的.

理解Callable:
CallableRunnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用.FutureTask用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.

理解FutureTask:
可以为想象去吃麻辣烫, 当餐点好后, 后厨就开始做了, 同时前台会给你一张 “小票”, 这个小票就是FutureTask, 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.

使用Thread类的构造器创建线程的时候, 传入的引用不能是Callable类型的, 而应该是FutrueTask类型, 因为构造器中传入的任务类型需要是一个Runnable类,CallableRunnable是没有直接关系的, 但FutrueTask类实现了Runnable类, 所以要想使用Callable创建线程, 我们就需要先把实现Callable接口的对象引用传给FutrueTask类的实例对象, 再将FutrueTask实例传入线程构造器中.

接下来,我们使用Callable实现 创建线程计算 1 + 2 + 3 + … + 1000

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Callable来计算1+2+3+4+...+1000
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for(int i = 0;i <= 10;i++){
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        //获取执行结果
        Integer sum = futureTask.get();
        System.out.println(sum);

    }
}
  • 创建一个匿名内部类,实现Callable接口.Callable带有泛型参数。泛型参数表示返回值的类型。
  • 重写Callablecall方法,完成累加的过程,直接通过返回值结算结果。
  • callable实例使用FutureTask包装一下。
  • 创建线程,线程的构造方法传入FutureTask.此时新线程就会执行FutureTask内部的Callablecall方法,完成计算,计算结果就放在了FutureTask对象中。
  • 在主线程中调用FutureTask.get();能够阻塞等待新线程计算完毕. 并获取到FutureTask中的结
    果.

相关面试题

介绍下 Callable 是什么

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算
结果.
Callable Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用.FutureTask用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.

ReentrantLock(可重入锁)

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁
  • unlock(): 解锁

以上述trylock为例:

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo30 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        boolean result = reentrantLock.tryLock();
        try{
            if(result){

            }else{

            }
        }finally {
            if (result){
                reentrantLock.unlock();
            }
        }
    }
}

ReentrantLock synchronized 的区别:

  • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准
    库的一个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
    但是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就
    放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true开启
    公平锁模式.
ReentrantLock reentrantLock = new ReentrantLock(true);
  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一
    个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指
    定的线程.

结论:虽然ReentrantLock有一定的又是,但是在实际开发中,大部分情况下还是使用Synchronized

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配trylock更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock

原子类

原子类内部用的是CAS实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo31 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);

        //使用原子类来解决线程安全问题
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();// count++
                // count.incrementAndGet(); // ++count
                // count.getAndDecrement(); // count--
                // count.decrementAndGet(); // --count
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}

在这里插入图片描述

信号量Semaphore

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.

举个🌰: 停车场的车位,是有固定上限的。
很多停车场会在入口显示一个牌子,牌子上面写:当前空闲车位有xx个(这个牌子,就相当于Semaphore)。
每次有车,从入口进去,计数器就-1
每次有车,从出口出来,计数器就+1
如果当前停车场里面的车满了,计数器就是0.
此时,如果还有车想停,有两种方式:
(1)等待其他车出去
(2)放弃这里,去别的停车场

P操作:申请一个可用资源,计数器就-1
V操作:释放一个可用资源,计数器就+1
P操作要是计数器为0了,继续P操作,就会出现阻塞等待。

考虑一个计数初始值为1的信号量
针对这个信号量,就只有1和0两种取值。(信号量不能是负的)
执行一次P(acquire)操作,1->0
执行一次V(release)操作,0->1
如果已经进行一次P操作了,继续进行P操作,就会阻塞等待。

锁是信号量的一种特殊情况,信号量是锁的一般表达。锁可以看为计数器是1的信号量(二元信号量)

CountDownLatch

假设有一场跑步比赛:
在这里插入图片描述
这场比赛,开始时间使明确的(裁判的发令枪)
结束时间,则是不确定的、(所有选手都冲过终点比赛才算结束)
为了等待这个跑步比赛结束,就引入了这个CountDownLatch

主要是两个方法:

  1. await(wait->等待 ,a->all)主线程来调用这个方法
  2. countDown表示选手冲过了重点线
    countDown在构造的时候,指定一个计数(选手的个数)

CountDownLatch类常用方法:

  • 构造方法
public CountDownLatch(int count)构造实例对象, count表示CountDownLatch对象中计数器的值
  • 普通方法
public void await() throws InterruptedException使所处的线程进入阻塞等待, 直到计数器的值清零
public void countDown()将计数器的值减1
public long getCount()获取计数器最初的值

上述例子中,有五个选手进行比赛,初始情况下每个选手都会冲过终点,都会调用countDown方法。
前四次调用countDownawait没有任何影响
第五次调用countDownawait被唤醒。(解除阻塞),此时就可以认为是整个比赛都结束了。

import java.util.concurrent.CountDownLatch;

public class ThreadDemo32 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(()->{
                try {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() +"跑到了终点");
                    latch.countDown();//调用countDown的次数和个数一致
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println();
            });
            t.start();
        }

        latch.await();
        System.out.println("比赛结束!");
    }
}

在这里插入图片描述

线程安全的集合类

原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.

多线程环境使用 ArrayList

  1. 自己加锁,自己使用synchronized或者ReentrantLock
  2. Collections.synchronized 这里会提供一些ArrayList相关的方法,同时是带锁的
  3. CopyOnWriteArrayList:简称COW,也叫做“写时拷贝”
    如果针对这个ArrayList进行读操作,不做任何额外的工作。
    如果进行写操作,则拷贝一份新的ArrayList,针对新的进行修改,修改过程中如果有读操作,就继续读旧的这份数据,当修改完毕了,使用新的替换旧的(本质上是一个引用之间的赋值,原子的)

很明显,这种方案,有点是不需要加锁,缺点则是要求这个ArrayList不能太大。适用于数组比较小的情况下。

比如:
服务器程序的配置维护:
一个程序可能包含很多的子功能,有的功能想要使用,有的不想要使用,有的希望功能应用不同的形态,就可以使用一系列的“开关选型”来控制当前这个程序的工作状态。
服务器程序的配置文件,可能会需要进行修改。修改配置可能就需要重启服务器才能生效。但是重启的操作可能成本比较高。

假设一个服务器重启需要耗时5min,如果有20台服务器,就需要100min。

因此,很多服务器,都提供了“热加载”(reload)这样的功能,通过这样的功能就可以不重启服务器,实现配置的更新。热加载的实现,就可以使用刚才所说的 写时拷贝 思路。

新的配置放到新的对象中,加载过程中,请求任然基于旧配置进行工作。当新的对象加载完毕,使用新对象替代旧对象。(替换完成之后,旧的对象就可以释放了)

多线程使用队列

  • ArrayBlockingQueue 基于数组实现的阻塞队列
  • LinkedBlockingQueue 基于链表实现的阻塞队列
  • PriorityBlockingQueue基于堆实现的带优先级的阻塞队列
  • TransferQueue 最多只包含一个元素的阻塞队列

多线程使用哈希表(重点)

HashMap是线程不安全的。
HashTable是线程安全的。(给关键方法加了Synchronized)
更推荐使用的是ConcurrentHashMap:更优化的线程安全哈希表。
考点:ConcurrentHashMap进行了哪些优化?比HashTable好在哪里?和HashTable之间的区别是什么?

  1. 最大的优化之处:ConcurrentHashMap相比于HashTable大大缩小了锁冲突的概率,把一把大锁,转化成多把小锁了。
    HashTable做法是直接在方法上加synchronized,等于是给this加锁,只要操作哈希表上的任何元素,都会产生加锁,也就有可能发生锁冲突。
    但是实际上,仔细思考不难发现,其实基于哈希表的结构特点,有些元素在进行并发操作的时候,是不会产生线程安全问题的,也就不需要使用锁控制。

在这里插入图片描述

此时,元素1,2在同一个链表上。如果线程A修改元素1,线程B修改元素2,就可能会有线程安全问题。(比如这两个元素相邻,此时并发删除/插入,就需要修改这两个节点相邻的节点的next的指向)
如果线程A修改元素3,线程B修改元素4不会有线程安全问题。这个情况是不需要加锁的。
HashTable,锁冲突概率就大大增加了,任何两个元素的操作都会有锁冲突,即使是在不同链表上。

ConcurrentHashMap做法是:每个链表有各自的锁。(而不是大家公用一把锁)
具体来说,就是使用每个链表的头结点作为锁对象。(两个线程针对同一个锁对象加锁,才有竞争,才有阻塞等待,针对不同对象,没有锁竞争)
在这里插入图片描述
此时,锁的粒度变小了。针对1,2。是针对同一把锁进行加锁,会有锁竞争,会保证线程安全。
针对3,4.是针对不同的锁进行加锁,不会有锁竞争了,没有阻塞等待,程序就会更快。(快是相对的)

上图中的情况, 是针对JDK1.8及其以后的情况, 而JDK1.8之前, ConcurrentHashMap使用的是 “分段锁”, 分段锁本质上也是缩小锁的范围从而降低锁冲突的概率, 但是这种做法不够彻底, 一方面锁的粒度切分的还不够细, 另一方面代码实现也更繁琐.

在这里插入图片描述
2. ConcurrentHashMap做了一个激进的操作:针对读操作,不加锁,只针对写操作加锁。
读和读之间没有冲突
写和写之间有冲突
读和写之间没有冲突(很多场景下,读写之间不加锁控制,可能就读到了一个写了一半的操作,如果写操作不是院子的,此时读就可能会读到写了一般的数据,相当于脏读)针对此情况可以使用volatile+原子的写操作。

3. ConcurrentHashMap内部充分地使用了CAS,通过这个也来进一步的削减加锁操作的数目。比如维护元素个数
4. 针对扩容,采取了“化整为零”的方式。
HashMap/HashTable扩容:
创建一个更大的数据空间,把旧的数组上的链表上的每个元素搬运到新的数组上。(删除+插入)
这个扩容操作会在某次put的时候进行触发
如果元素个数特别多,就会导致这样的搬运操作比较耗时。
就会出现:某次put比平时的put卡很多倍。

ConcurrentHashMap中,扩容采取的是每次搬运一小部分元素的方式。
创建新的数组,旧的数组也保留。
每次put操作,都会往新数组上添加,同时进行一部分搬运(把一小部分旧的元素搬到新数组上)
每次get的时候,则旧数组和新数组都查询
每次remove的时候,只是把元素删了就行。

经过一段时间后,所有的元素都搬运好了,最终再释放旧数组。

相关面试题

  1. ConcurrentHashMap的读是否要加锁,为什么?

读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了
volatile 关键字.

  1. 介绍下 ConcurrentHashMap的锁分段技术?

这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

  1. ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对 象). 将原来 数组 + 链表的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8 个元素)就转换成红黑树.

4) HashtableHashMap、ConcurrentHashMap 之间的区别?

HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
CAS 机制. 优化了扩容方式. key 不允许为 null

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

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

相关文章

2.sql server数据表的管理(实验报告)

目录 一﹑实验目的 二﹑实验平台 三﹑实验内容和步骤 四﹑命令(代码)清单 五﹑运行结果 一﹑实验目的 掌握使用SQL Server管理平台和Transact-SQL语句Create table和Alter table创建和修改表的方法&#xff1b;掌握在SQL Server管理平台中对表进行插入、修改和删除数据操作…

哪种蓝牙耳机戴着最舒服?久戴不痛的蓝牙耳机推荐

很多喜欢跑步或通勤的时候带着耳机听音乐&#xff0c;而现在无线耳机市场规模扩大之后&#xff0c;也开始走向更加细分的市场&#xff0c;以满足越来越不同的差异化需求&#xff0c;但是佩戴的舒适度是很多人关注的&#xff0c;下面整理了几款佩戴舒适度高的蓝牙耳机&#xff0…

国考省考结构化面试:组织管理题,调研题,宣传题,活动题,整治题

国考省考结构化面试&#xff1a;组织管理题&#xff0c;调研题&#xff0c;宣传题&#xff0c;活动题&#xff0c;整治题 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测…

基于python语言dlib库和opencv库的视频眨眼检测

功能说明&#xff1a; 基于python编程语言&#xff0c;使用dlib 和opencv开发的视频眨眼检测。 环境&#xff1a; * python 3.6.8 * opencv 3.4.2.16 * dlib 19.7.0 原理&#xff1a; 1.使用opencv-python读取处理视频图像 2.使用线程机制处理人脸检测关键点 3.根…

Midjourney 5.1震撼更新!逼真到给跪,中国情侣细节惊艳,3D视频大片马上来

来源 | 新智元 作者 | 桃子&#xff0c;拉燕 一个月前&#xff0c;Midjourney V5画的一对中国完美情侣在网上爆火&#xff0c;让许多人纷纷惊呼画师要失业了。 恰在今天&#xff0c;Midjourney官宣V5能免费用了&#xff0c;而且还是最新版本V5.1。 各个大模型的研究测试传送门 …

Android 13 变更及适配攻略

准备工作 首先将我们项目中的 targetSdkVersion和compileSdkVersion 升至 33。 影响Android 13上所有应用 1.通知受限 对新安装的应用的影响&#xff1a; 如果用户在搭载 Android 13 或更高版本的设备上安装您的应用&#xff0c;应用的通知默认处于关闭状态。在您请求新的…

ChatGPT如何生成可视化图表-示例中国近几年出生人口

本教程收集于&#xff1a;AIGC从入门到精通教程汇总 ChatGPT本身不能直接生成可视化图表&#xff0c;但可以配合其他可视化工具或库 方法一&#xff1a;前端可视化开发库 Echarts&#xff08;地址&#xff1a;Apache ECharts &#xff09; 方法二&#xff1a;现有Python库。…

dump_stack分析函数调用关系实例及其实现

dump_stack分析函数调用关系 文章目录 dump_stack分析函数调用关系一、dump_stack实例二、dump_stack实现分析Step 1: dump_stack_print_infoStep 2: show_stack 三、关于堆栈 一、dump_stack实例 在正点原子阿尔法开发板中查看insmod命令使用什么方法&#xff1a; #include …

[蓝帽杯 2022 初赛]之Misc篇(NSSCTF)刷题记录(复现)⑨

NSSCTF-Misc篇-[蓝帽杯 2022 初赛] 计算机取证&#xff1a;[蓝帽杯 2022 初赛]计算机取证_1[蓝帽杯 2022 初赛]计算机取证_2[蓝帽杯 2022 初赛]计算机取证_3[蓝帽杯 2022 初赛]计算机取证_4 手机取证&#xff1a;[蓝帽杯 2022 初赛]手机取证_1[蓝帽杯 2022 初赛]手机取证_2 网站…

使用堆视图创建3个按钮三角摆放

使用堆视图创建3个按钮三角摆放 效果如图&#xff1a; 分析 从效果图看&#xff0c;想要让3个Button呈三角摆放&#xff0c;需要两个堆视图完成。 首先外部一个大的“垂直堆”&#xff0c;垂直堆第一项放一个Button&#xff0c;第二项放一个“水平堆” 水平堆里再放两个But…

刚入职领导就要求做自动化测试?我懵了,从业务到框架设计总结...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

深度学习的环境搭建(window+pytorch)

1.检查是否安装CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由 NVIDIA 推出的一种并行计算平台和编程模型&#xff0c;用于利用 NVIDIA GPU&#xff08;Graphics Processing Unit&#xff09;的强大计算能力进行高性能计算任务。CUDA 的主要特点是…

logstash介绍和使用-ELK文章2

官方 Logstash 是免费且开放的服务器端数据处理管道&#xff0c;能够从多个来源采集数据&#xff0c;转换数据&#xff0c;然后将数据发送到您最喜欢的“存储库”中。 下载和文档&#xff1a;https://www.elastic.co/cn/logstash/ docker部署&#xff1a;https://hub.docker.…

教你快速把heic格式转化jpg,4种方法操作简单

教你快速把heic格式转化jpg的方法&#xff0c;因为HEIC格式图片通常出现在苹果公司的iOS 11操作系统及之后的版本中&#xff0c;这是因为苹果公司在这些版本中采用了HEIF&#xff08;高效图像格式&#xff09;作为默认的照片格式来替代JPEG格式。同时&#xff0c;需要注意的是&…

asp.net+sqlserver社区小区流动人口管理系统

该系统的基本功能包括用户登录&#xff0c;管理员信息管理&#xff0c;社区组织管理&#xff0c;常住人口管理&#xff0c;流动人口管理&#xff0c;社区事务管理&#xff0c;社区服务管理&#xff0c;系统用户管理&#xff0c;修改密码等功能。 &#xff08;3&#xff09;功能…

项目分析v2

用户&#xff1a; 登录&#xff1a; 不能重复登录。 在服务端使用一个hashset记录用户的登录状态&#xff0c;如果用户id不在集合里面&#xff0c;就可以登录&#xff0c;登录时将用户id添加到集合中。用户下线时&#xff0c;将set中的元素删除。 登录成功后&#xff0c;服务端…

【语义分割】LinkNet从0到1和代码实现

文章目录 前言1.网络结构1.1 网络结构示意图1.2 创建LinkNet模型 2.代码2.1 各模块搭建2.1.1 卷积模块2.1.2 反卷积模块2.1.3 编码器模块 2.2 编码网络结构2.3 损失函数&训练2.4 训练 前言 已经有了U-net了&#xff0c;为什么需要linkNet&#xff1f; unet见这个文章【语义…

Docker基础篇(很详细)

一、简单介绍 &#xff08;一&#xff09;为什么用docker 开发人员发开完成就发布一个jar或者war包&#xff0c;其他的都交给运维人员来做&#xff1b;而现在&#xff0c;开发即运维&#xff0c;打包部署上线一套流程走完&#xff1a;开发人员会将项目及其附带的环境一起打包j…

UML类图使用介绍

文章目录 一、UML图1、什么是UML图2、类图概述3、类图的作用 二、类的表示方式举个栗子 三、类与类之间关系的表示方式1、关联关系&#xff08;1&#xff09;单向关联&#xff08;2&#xff09;双向关联&#xff08;3&#xff09;自关联 2、聚合关系3、组合关系4、依赖关系5、继…

安全加密基础—基本概念、keytool、openssl

前言 &#xff08;1&#xff09;本文不涉及源码、底层。只是讲解大概的密码演变过程和基本概念。能让接触到相关名词的人知道这些名词是干嘛的&#xff0c;为什么要有它。专业人士可以当作概念梳理&#xff0c;非专业人士可以当作科普。 &#xff08;2&#xff09;本文你将了解…