多线程 (上) - 学习笔记

前置知识

什么是线程和进程?

进程: 是程序的一次执行,一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程: 进程中的一个执行流(控制单元 / 执行任务),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

线程的优点

  1. 轻量, 创建一个线程的代价要比进程小的多
  2. 线程之间的切换, 对比进程, OS 要做的工作小很多
  3. 线程运行, 占用资源比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速 I/O 操作同时, 可执行其他任务
  6. 计算密集型应用, 可将计算分解到多个线程中实现, 以便在多处理器系统上运行
  7. I/O 密集型应用, 可将 I/O 操作重叠, 一个线程等待多个不同的 I/O 操作, 以提高性能.

二者的区别和联系

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
  • 进程有自己的内存地址空间, 线程只独享指令流执行的必要资源, 如寄存器和栈
  • 线程的创建, 切换, 终止效率更高 .

更轻量的追求

人们不满足于线程的轻量, 因此又有了 “线程池” (ThreadPool) 和 “协程” (Coroutine) .

ThreadPool : 是一种利用池化技术思想来实现线程管理的技术, 主要是为了复用线程.
简单理解就是, 创建了一个容器, 容器里面放的是一定量的线程, 每次使用线程的时候, 不用创建, 直接从容器中取一个线程用, 用完之后不用销毁, 再放到回容器里去,以备下次使用

协程运行在线程之上, 属于是在线程基础之上通过分时复用的方式运行多个协程.
即一个线程包括多个协程, 协程可以当更小的线程取用, 并且协程的状态切换比线程更轻量 .

Java 线程和 OS 线程的关系

线程是 OS 的概念, OS 内核实现了线程这样的机制, 并且对用户层提供了一些 API 以供使用.
Java 标准库中的 Thread 类可以视为是对 OS 提供的 API , 进行了进一步的抽象和封装, 以便使用 .

运行 DEMO

运行代码

import java.util.Random;

public class test1 {
    private static class MyThread extends Thread{
        @Override
        public void run() {
            Random random = new Random();
            while(true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止 0-9 秒
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

        Random random = new Random();
        while(true) {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                // 随机停止 0-9 秒
                Thread.sleep(random.nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

main
Thread-0
Thread-2
Thread-1
Thread-1
Thread-0
main
Thread-2
Thread-2
Thread-1
Thread-1
main
Thread-0
Thread-0
Thread-0
Thread-1
main
Thread-1
Thread-2
Thread-0
...

从运行结果可以看出, 主线程与子线程之间的运行顺序完全随机 .

线程创建

继承 Thread 类

  1. 线程类继承 Thread
private static class MyThread extends Thread{
   @Override
    public void run() {
        System.out.println("线程运行 逻辑");
    }
}
  1. 创建线程类的实例
MyThread t = new MyThread();  //此时只是声明了我要创建子线程, 并没有真正的去分配资源啥的

3.调用 start 方法, 才真的在操作系统的底层创建出一个线程

t.start(); //真正给线程分配资源

实现 Runnable 接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行 逻辑");
    }
}
  1. 创建 Thread 类实例, 调用 Thread 时构造方法将 Runnable 对象作为 target 参数 .
Thread t = new Thread(new MyRunnable());
  1. 调用 start 方法, 才真的在操作系统的底层创建出一个线程
t.start();

对比上述两种方式, 若要表示本子线程

  • 继承 Thread 类, 直接使用 this 则表示当前线程对象的引用
  • 实现 Runnable 接口, this 表示的时 MyRunnable 的引用, 若想表示本子进程, 需要使用 Thread.currentThread()

匿名内部类创建 Thread 子类对象

Thread t1 = new Thread() {
    @Override
    public void run() {
        super.run();
    }
};

匿名内部类创建 Runnable 子类对象

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("内部代码逻辑");
    }
});

lambda 表达式创建 Runnable 子类对象

Thread t3 = new Thread(() -> System.out.println("内部代码逻辑"));
Thread t4 = new Thread(() -> {
    System.out.println("内部代码逻辑");
});

Thread 类常用方法

构造方法

在这里插入图片描述

Thread 常见的属性

在这里插入图片描述

  • ID 是线程的唯一标识, 不会重复
  • 优先级高的线程, 理论上 更容易被调用到
  • 后台线程的话记住一点: **JVM会在一个进程的所有 非后台线程 结束后, 才会结束运行 **
  • 存活代表 run 方法是否运行结束

中断进程

常见两种方式(本质上没什么区别)

  1. 自定义一个共享标记
  2. 使用 interrupt() 方法来通知 (相当于系统定义的共享标记)
使用自定义的变量来作为标志位
public class test2 {
    private static class MyRunnable implements Runnable{
        private static boolean  isQuit = true;

        @Override
        public void run() {
            while(isQuit) {
                System.out.println("线程执行");

                try {
                    // 线程执行中
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 共享标记修改, 需要终止线程
            System.out.println("线程终止");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread t = new Thread(target, "zrj");
        t.start();
        Thread.sleep(10 * 1000);

        System.out.println("需要在此刻终止线程的运行!");
        target.isQuit = false;
    }
}
使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted()

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

在这里插入图片描述

public class test3 {
    private static class MyRunnable implements Runnable{
        @Override
        public void run() {
            // 如果该标记位没有被设置, 即没有被中断
            while(!Thread.interrupted()) {
                System.out.println("进程执行");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("进程阻塞!");
                    break;
                }
            }

            // 标记位被设置, 进程被打断
            System.out.println("进程终止");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread t = new Thread(target, "lty");
        System.out.println("进程执行!");
        t.start();
        Thread.sleep(10 * 1000);

        System.out.println("打断进程!即设置标记位");
        t.interrupt();
    }
}

Thread 收到通知的方式有两种
1.线程因为调用 wait / join / sleep 等方法引起的阻塞, 以异常抛出的方式通知, 清除中断标记
2.如果是内部中断标记被设置, thread 可以通过 两个判断方法来收到通知, 该方式收到通知更及时, 即使线程正在 sleep 也可以马上收到

等待一个线程 - join

在这里插入图片描述

public class test4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i=1 ; i<=10; i++) {
                try {
                    System.out.println("线程执行中!");

                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 线程执行结束!");
        };

        Thread t1 = new Thread(target, "zrj");
        Thread t2 = new Thread(target, "lty");

        // t1 开始执行
        t1.start();
        // t1 挂起
        t1.join();

        // t2 开始执行
        t2.start();
        // t2 挂起
        t2.join();
    }
}

attention : 对于 join 挂起的线程, 如果没有被唤醒的话, 将永久不会被调用执行

获取当前线程引用

在这里插入图片描述

休眠当前线程

在这里插入图片描述

线程的状态

NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.

进程状态之间的转换

在这里插入图片描述

学校教学课本上的图片展示是这样的 (算是一个简略版本)

在这里插入图片描述

给出几个注意的点 :
BLOCKED 表示被锁住状态 ; WAITING 和 TIMED_WAITING 表示等待唤醒状态 .
TIMED_WAITING 线程在等待唤醒, 但设置了时限 ; WAITING 线程没有设置时限 (死等)
Thread.yield() 调用后, 不会改变进程状态, 但会立即让出 CPU, 重新去就绪队列排队 .

线程安全

什么是线程安全?

如果 多线程和单线程环境下 运行的结果相同, 那么我们就说它是线程安全的 .

线程不安全的原因

  1. 修改共享数据
  2. 原子性 (同步互斥)
  3. 可见性
  4. 代码顺序性

什么是原子性?
执行的最小单元

什么是可见性
一个线程对共享变量值的修改, 能够及时的被其他线程看到
在这里插入图片描述
主内存就说硬盘角度的 “内存”, 工作内容可以认为是 cache / 寄存器
因为 CPU 对 cache / 寄存器的访问速度要比内存 快 3-4 个数量级. 而且有些操作需要连续访问 N 次某个变量, 读一次放回去一次速度很慢, 因此我们可以第一次读的时候给放到 寄存器 里, 后续的访问都只访问寄存器, 效率就会大大提升

Java 内存模型 (JMM) : Java 虚拟机规范中定义了 Java 内存模型
目的是屏蔽掉各种硬件和 OS 的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致的并发效果

代码顺序性
编译器会自动对单线程下的代码进行代码重排序, 遵循的前提是 “保持逻辑不发生变化”, 但是在多线程环境下该前提很难遵守

synchronized 关键字

特性 :

  1. 互斥
  2. 刷新内存 (即保证内存可见性)
  3. 可重入

互斥

synchronized 底层是用 OS 的 mutex lock 实现的
互斥的含义是 每个被 synchronized 维护的临界资源, 不会被多个线程同时执行到 .

某个线程执行

  • 进入 synchronized 修饰的代码块, 相当于加锁
  • 退出 synchronized 修饰的代码块, 相当于解锁

阻塞等待
针对每一把锁, OS 内部都会维护一个等待队列, 当这把锁被某个线程占有的时候, 其他线程再来竞争这把锁, 就上不了锁, 会在队列里等待, 直到之前的线程解锁, 再由 OS 随机唤醒一个 等待队列里的线程来使用这把锁 (没有什么先来后到, 一切随机顺序, 先来的也可能得等很长时间 [你喜欢一个妹子, 追了很久,但不是说, 你先喜欢的, 就是你先谈, 人家就是先喜欢上了别人, 就是一眼万年的和别人在一起了, 你也没辙~~]).

翻译翻译, 什么叫做 TM 的可重入?

可重入 和 不可重入
一个线程中, 对一个对象上了两次锁, 并且中间没有释放锁过程

lock();  //第一次
lock();  //第二次

如果是不可重入锁, 由于第一次加锁, 并没有解锁, 所以第二次加锁会失败, 即该线程会在阻塞队列等待, 但是因为第一次锁的解锁过程一定在这个线程后面的某个地方, 就会产生死锁 (卡死在等待队列, 出不来了 [我卡我自己])
可重入锁呐, 就是会自带一个标识类的对象, 第二次加锁之前会判断该线程是不是之前上锁的线程, 如果是, 那你就进去吧 (eg : 你爸回家了, 如果你要进去, 你爸会给你开门, 如果是不认识的人要进去, 你爸就不会开门)

volatile 关键字

特性: 保证修饰变量的内存可见性

在这里插入图片描述

代码在写入 volatile 修饰的变量的时候

  • 改变线程工作内存的值
  • 刷新主内存的值

代码在读取 volatile 修饰的变量的时候

  • 先读一下主内存的值, 更新工作内存
  • 再从工作内存读取值使用

synchronized 和 volatile 有本质区别
synchronied 保证的是原子性, 衍生出内存可见性这个性质
volatile 保证的是内存可见性, 只是用的时候, 不会读取错误

wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间的先后顺序难以预知
但是我们有方法可以协调多个线程之间的执行先后顺序

  • wait() / wait(long timeout) : 让当前线程进入等待状态
  • notify() / notifyAll() : 唤醒在当前对象上等待的线程

notify() : 随机唤醒一个在当前对象上等待的线程
notifyAll() : 唤醒在当前对象上等待的所有线程
attention : wait() / notify() / notifyAll() 均为 Object 类的方法

wait()

wait 做的事

  • 把当前线程放到等待队列中去
  • 释放当前锁
  • 满足一定条件被唤醒, 重新尝试获取这个锁

wait 要搭配 synchronized 来使用, 脱离 synchronized 使用 wait 会直接抛出异常 .

wait 结束条件

  • 其他线程调用该对象的 notify 方法将其唤醒
  • wait 等待时间超时
  • 其他线程调用该等待线程的 interrupted 方法, 导致其 wait 抛出 InterruptedException 异常

notify() 方法

随机唤醒一个, 指定对象的等待队列中的线程
attention : 在 notify() 方法后, 当前线程不会马上释放该对象锁, 要等到执行 notify() 方法的线程将程序执行完, 也就是退出同步代码块之后, 才会释放对象锁 (即确保有线程被唤醒之后, 才会释放原本的锁)

notifyAll() 方法

有个注意点, 虽然 notifyAll() 是唤醒当前对象等待队列中的所有线程, 但是这些线程还是需要竞争锁, 所有虽然全部唤醒, 但是并不是同时执行, 仍然是一个一个的执行 .

wait & notify 示例代码

public class test5 {
    static class waitTask implements Runnable{
        private Object locker;

        public waitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class notifyTask implements Runnable {
        private Object locker;

        public notifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new waitTask(locker));
        Thread t2 = new Thread(new notifyTask(locker));
        
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

wait & slepp

相同点 : 都可以让线程放弃执行一段时间
不同点 :

  • wait 用于线程之间的通信, sleep 用于让线程阻塞
  • wait 需要搭配 synchronized 使用, sleep 不需要
  • wait 是 Object 类的方法, sleep 是 Thread 的静态方法

多线程相关的几个设计模式

单例模式

单例模式就是全局范围内, 该对象只有一个实例

饿汉版本的单例模式 (声明的同时就创建)

class Singleton{
    private static Singleton instance = new Singleton();   
    private Singleton() {}
    private static Singleton getInstance() {
        return instance;
    }
}

懒汉版本的单例模式 (先声明, 什么时候用到, 什么时候创建)

class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉模式的多线程版本

class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public synchronized static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

对于上述版本, 你会发现每次使用的时候都会被加锁, 花销会很大, 因此对此进行改进

class Singleton{
    private volatile static Singleton instance = null; // volatile 保证内存可见性
    private Singleton(){}
    public static Singleton getInstance() {
        if(instance == null) { // 加锁/解锁开销比较高, 这里判断只对首次创建实例的时候进行加锁.
	        synchronized (Singleton.class) {
				if(instance == null) { // 首次创建完实例后, 仍有很多线程排队在等待队列, 用这个判断让其他等待队列中的线程结束
					instance = new Singleton();
				}
			}
        }
        return instance;
    }
}

阻塞队列

阻塞队列是什么?
特殊的一种队列, redis 中的 blpop, brpop 也使用了阻塞思想.
既然是队列, 就遵循先进先出思想
阻塞队列是一种线程安全的数据结构,具有特性如下 :

  • 当队列满, 继续入队列就会阻塞, 直到队列中有空余位置
  • 当队列空, 继续出队列就会阻塞, 直到队列中有元素

典型应用场景 : 生产者消费者模型

定时器

达到某个时间, 就执行某块代码

标准库中的定时器

Timer 类, 核心方法为 schedule .
schedule 包含两个参数, 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间后执行 .

Timer timer= new Timer();
timer.schedule(new TimerTask() {
     @Override
     public void run() {
         System.out.println("hello lty!");
     }
}, 3000);

线程池

线程池最大的好处就说, 减少每次启动, 销毁线程的损耗

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(N) 可以创建出容量为 N 的线程池, 内含 N 个线程
    返回值为 ExecutotService.submit 可以注册一个任务到线程池中
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello lty!");
    }
});

创建线程池的几种方式 (Executors 本质上是对 ThreadPoolExecutor 类的封装)

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计算机操作系统原理分析期末复习

一、理解与识记 三种基本的OS类型及各自的特点&#xff1a; 批处理系统&#xff08;内存同时存放几个作业。优点&#xff1a;资源利用率高、作业吞吐量大、系统开销小&#xff1b;缺点&#xff1a;用户无交互性、作业平均周转时间长&#xff09;、分时系统&#xff08;时间片技…

后端项目全局异常处理-使用RuntimeException自定义异常异常分类简单举例

接上篇&#xff1a;后端项目操作数据库-中枢组件Service调用Mapper 自定义异常&#xff1a; 手动抛出异常&#xff0c;为了后续统一捕获&#xff0c;需要异常自定义&#xff1b; 如&#xff1a;当使用抛出异常的方式表示“操作失败”时&#xff0c;为了后续统一捕获&#xff0c…

信息过载的反思

在今天微信、短视频、图文不停的密集的信息轰炸之下&#xff0c;你“察觉”到你的精力不济没有&#xff1f;你时常会觉得耳鸣、目涩&#xff0c;注意力无法集中&#xff1b;你懒于记忆&#xff0c;甚至爱人的手机号都想不起来&#xff0c;习惯于用移动电话找人名&#xff0c;不…

Docker - Android源码编译与烧写

创建源代码 并挂载到win目录 docker run -v /mnt/f/android8.0:/data/android8.0 -it --name android8.0 49a981f2b85f /bin/bash 使用 docker update 命令动态调整内存限制&#xff1a; 重新运行一个容器 docker run -m 512m my_container 修改运行中容器 显示运行中容器 d…

使用飞书自定义机器人发送消息

使用飞书机器人可以很方便的获取自动化任务的反馈&#xff1a; 在群里创建一个机器人&#xff1a; 记住下面的 webhook地址&#xff0c;这个是标识机器人的唯一ID&#xff0c;比如它的webhook地址是&#xff1a;"https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx-a…

【1.计算机组成与体系结构】流水线技术

目录 1.流水线的定义2.相关参数计算2.1 流水线计算公式2.2 流水线的吞吐率2.3 流水线加速比计算 3.超标量流水线 1.流水线的定义 流水线是指在程序执行时多条指令重叠进行操作的一种准并行处理实现技术。各种部件同时处理是针对不同指令而言的&#xff0c;它们可同时为多条指令…

STM32——超声波传感器

需求&#xff1a; 使用超声波测距&#xff0c;当手离传感器距离小于 5cm 时&#xff0c; LED1 点亮&#xff0c;否则保持不亮状态 接线&#xff1a; 定时器配置&#xff1a; 使用 TIM2 &#xff0c;只用作计数功能&#xff0c;不用作定时。 将 PSC 配置为 71 &#xff0c;…

笔记 - 现代嵌入式芯片封装识读

0.引用&#xff1a; 配图、资料并非一处采集&#xff0c;我不太容易找到图片的原始链接。这里的图片仅作示例&#xff0c;无商业用途。如果涉及侵权&#xff0c;请随时联系。谢谢&#xff01; PCB封装欣赏了解之旅&#xff08;下篇&#xff09;—— 常用集成电路_ufqfpn封装…

什么是Z-Wave,技术特点,各国支持的频段

1.1 背景 Z-Wave是一种无线通信的协议&#xff0c;主要应用于智能家居网络&#xff0c;而其Z-Wave联盟主要是推动家庭自动化领域采用Z-Wave协议&#xff0c;其联盟成员都是智能家居领域非常有名的厂商&#xff0c;基本上覆盖了全球。 2.1 技术特点 低功耗、高可…

『App自动化测试之Appium应用篇』| 元素定位工具Appium-Inspector从简介、安装、配置到使用的完整攻略

『App自动化测试之Appium应用篇』| 元素定位工具Appium-Inspector从简介、安装、配置到使用的完整攻略 1 Appium-Inspector简介2 Appium Desktop中的Appium-Inspector3 安装Appium-Inspector4 Appium-Inspector网页版5 Appium-Inspector界面说明5.1 Appium Server配置5.2 Selec…

飞致云与上海吉谛达成战略合作,获得Gitea企业版中国大陆地区独家代理权

2023年12月13日&#xff0c;中国领先的开源软件提供商FIT2CLOUD飞致云宣布与上海吉谛科技有限公司&#xff08;以下简称为上海吉谛&#xff09;正式达成战略合作&#xff0c;FIT2CLOUD飞致云获得上海吉谛旗下代码托管平台Gitea企业版中国大陆地区独家代理权。 Gitea项目&…

使用 Pnpm 和 Vite 构建 Vue 项目

文章目录 本地 Node 环境安装 Pnpm 包管理工具使用 Vite 创建 Vite 官网&#xff1a;https://cn.vitejs.dev/ 本地 Node 环境 首先&#xff0c;确保已经安装了 Node.js 和 npm。可以在命令行中运行 node -v 和 npm -v 来检查它们是否已经正确安装&#xff1a; 安装 Node.js 通…

【Git 小妙招】走进 Git 的分支管理(万字图文讲解)

文章目录 前言1. 理解分支2. 创建分支3. 切换分支4. 合并分支5. 删除分支6. 合并冲突7. 分支管理策略7.1 一个简单的分支策略(仅参考) 8. bug 分支9. 删除临时分支总结 前言 本文开始介绍 Git 的杀手级功能之⼀&#xff1a;分⽀。本文涉及分⽀创建&#xff0c;切换&#xff0c…

2. 基础数据结构-数组

2. 基础数据结构-数组 2.1 概念 数组是一种数据结构&#xff0c;它是一个由相同类型元素组成的有序集合。在编程中&#xff0c;数组的定义是创建一个具有特定大小和类型的存储区域来存放多个值。数组可以是一维、二维或多维的。每个元素至少有一个索引或键来标识。 2.2 数组特…

Leetcode—113.路径总和II【中等】

2023每日刷题&#xff08;五十七&#xff09; Leetcode—113.路径总和II 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* …

【数谷·企声】贵州恩典集团:半年内实现上规入统,大力推广贵州酱酒品牌

近年来&#xff0c;贵阳贵安大力实施“数字活市”战略&#xff0c;数字产业高速增长&#xff0c;数字红利加速释放&#xff0c;营商环境持续优化&#xff0c;成功吸引了一批批优质企业落户&#xff0c;贵州恩典企业管理&#xff08;集团&#xff09;有限公司&#xff08;以下简…

DICOM 文件中,VR,VL,SQ,图像二进制的几个注意点

DICOM 文件的结构&#xff0c;在网上有很多的学习资料&#xff0c;这里只介绍些容易混淆的概念&#xff0c;作为回看笔记。 1. 传输语法 每个传输语法&#xff0c;起都是表达的三个概念&#xff1a;大小端、显隐式、压缩算法 DICOM Implicit VR Little Endian: 1.2.840.1000…

Linux 常用的操作命令

我们习惯的使用Windows,安装软件进行使用&#xff0c;比如 WPS&#xff0c;浏览器&#xff0c;一些工具&#xff0c;但是在Linux上就需要用命令去操作&#xff0c;也可以使用像Ubuntu 和 CentOS这类的可视化面板 Linux系统是开源的&#xff0c;所以开发人员可以反复的发现Bug以…

高项备考葵花宝典-项目进度管理核心方法加强理解-关键路径法

关键路径法&#xff08;Critical Path Method&#xff0c;CPM&#xff09;是一种基于数学计算的项目计划管理方法&#xff0c;是网络图计划方法的一种&#xff0c;属于肯定型的网络图。关键路径法将项目分解成为多个独立的活动并确定每个活动的工期&#xff0c;然后用逻辑关系&…

​hashlib --- 安全哈希与消息摘要​

源码&#xff1a; Lib/hashlib.py 本模块针对许多不同的安全哈希和消息摘要算法实现了一个通用接口。 包括了 FIPS 安全哈希算法 SHA1, SHA224, SHA256, SHA384, SHA512, (定义见 the FIPS 180-4 standard), SHA-3 系列 (定义见 the FIPS 202 standard) 以及 RSA 的 MD5 算法 (…