并发编程之深入理解Java线程

并发编程之深入理解Java线程

线程基础知识

线程和进程

进程

  • 程序由指令和数据组成、但这些指令要运行,数据要读写,就必须要将指令加载至CPU、数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程
  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是系统资源分配的最小单位。

线程

  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须由一个父进程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • 线程,有时也被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位
进程和线程的区别
  • 进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算的进程通信称为IPC(Inter-processcommunication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,因为他们共享进程内的内存,一个例子是多个进程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换低
进程间通信的方式
  • 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它允许无亲缘关系线程间进行通信
  • 信号(signal):信号是软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号于处理器收到一个中断请求效果上是一致的。
  • 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量优先的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取消息。
  • 共享内存(shared memory):最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中的数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  • 信号量(semaphore):主要作为进程之间及同一种进程之间得同步和互斥手段
  • 套接字(socket):这是一种更为一般得进程间得通信机制,它可用于网络中不同机器之间的进程间通信
线程的同步互斥
  • 线程同步是指线程之间具有一种制约关系,一个线程的执行依赖于另一个线程的消息,当它没有得到另一个线程的消息时应等待,知道消息到达时才会被唤醒。
  • 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排他性。当有若干个线程都要使用某一共享资源是,任何时刻最多只允许一个线程去使用,其他要使用该资源的线程必须要等待,直到占用资源者释放该资源。线程互斥可以看成一种特殊的线程同步。
四种线程同步互斥的控制方法
  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问(在一段时间内只允许一个线程访问的资源就是临界资源)
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的
  • 信号量:为控制一个具有有限数量用户资源而设计。
  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始
上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个线程或进程到另一个线程或进程的切换。

进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源

上下文是CPU寄存器和程序计时器在任何时间点的内容

寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。

程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统

image-20240214215412696

上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动

  • 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
  • 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它。
  • 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

image-20240214215657229

上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但他们可以通过系统调用运行部分内核代码。

内核模式(Kernal Mode) vs 用户模式(User Mode)

Kernal Mode

  • 在内核模式中,执行代码可以完全且不受现在地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪

User Mode

  • 在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。

image-20240214220302903

CPU保护模式

image-20240214220336301

应用程序以下几种情况会切换到内核模式:

  • 系统调用:当应用程序需要执行诸如读写文件、创建进程、建立网络连接等操作时,需要通过系统调用请求操作系统提供服务。
  • 异常事件:当程序执行出错,如除零错误、访问非法内存等,会触发异常,操作系统需要切换到内核态来处理这些异常。
  • 设备中断:当外部设备(如键盘、鼠标、网络接口卡等)发出中断信号时,操作系统需要切换到内核态来处理这些中断

上下文切换是多任务操作系统的一个基本特性。在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃他们在CPU中的事件,或者是调度器在进程耗尽其CPU时间片时进行切换的结果。

上下文切换通常是计算机密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,时间上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一就是它的上下文切换和模式切换成本极低。

通过命令查看CPU上下文切换情况

Linux可以通过命令统计CPU上下文切换数据

# 可以看到整个操作系统每1秒CPU上下文切换的统计
vmstat 1

image-20240214221800673

其中cs列就是CPU上下文切换的统计。CPU上下文切换不等价于线程切换,很多操作会造成CPU上下文切换。

  • 线程、进程切换
  • 系统调用
  • 中断
查看某一个线程\进程的上下文切换

使用pidstat命令

  • 常用的参数:

    -u 默认参数,显示各个进程的 CPU 统计信息

    -r 显示各个进程的内存使用情况

    -d 显示各个进程的 IO 使用

    -w 显示各个进程的上下文切换

    -p PID 指定 PID

# 显示进程5598每一秒的切换情况
pidstat -w -p 5598 1

image-20240214222213640

其中cswch表示主动切换,nvcswsh表示被动切换。

  • 从进程的状态信息中查看

    通过命令 cat /proc/5598/status 查看进程的状态信息

    voluntary_ctxt_switches: 40469351

    nonvoluntary_ctxt_switches: 2268

    这2项就是该进程从启动到当前总的上下文切换情况。

操作系统层面线程生命周期

操作系统层面的线程的生命周期基本上可以用下图“五台模型”来描述,这五态分别是:初始状态、可运行状态、运行状态、休眠状态和中止状态

image-20240214222636764

  • 初始状态:指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  • 可运行状态:指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  • 当有空闲CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态
  • 运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个时间(例如条件变量),那么线程的状态就会切换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  • 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

Linux

  • ps -ef 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

Java线程详解

Java线程的实现方式

方式1:使用Thread类或继承Thread类

// 创建线程对象
Thread t = new Thread() {public void run() {// 要执行的任务}
};
// 启动线程

方式2:实现 Runnable 接口配合Thread

Runnable runnable = new Runnable() {public void run(){// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程

方式3:使用有返回值的Callable

class CallableTask implements Callable<Integer> {@Overridepublic Integer call() throws Exception {return new Random().nextInt();}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());

方式4:使用lambda

new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终会调用Thread#run方法

Java线程实现的原理

Jva线程执行为什么不能直接调用run()方法,而是要调用start()方法

run()方法是普通方法调用,start()会创建线程进程调用

https://www.processon.com/view/link/5f02ed9e6376891e81fec8d5

Java线程属于内核级线程

JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。

内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。

image-20240214230110067

协程

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

image-20240214230154684

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。

  • 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。

  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。

Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要的调度方式分为两种,分别是协同式线程调度和抢占式线程调度。

协同式线程调度

  • 线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

抢占式线程调度

  • 每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程调度就是抢占式调度

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

public class SellTicketDemo implements Runnable {/*** 车票*/private int ticket;public SellTicketDemo() {this.ticket = 1000;}@Overridepublic void run() {while (ticket > 0) {synchronized (this) {if (ticket > 0) {try {// 线程进入暂时的休眠Thread.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// 获取到当前正在执行的程序的名称,打印余票System.out.println(Thread.currentThread().getName()+ ":正在执行操作,余票:" + ticket--);}}Thread.yield();}}public static void main(String[] args) {SellTicketDemo demo = new SellTicketDemo();Thread thread1 = new Thread(demo, "thread1");Thread thread2 = new Thread(demo, "thread2");Thread thread3 = new Thread(demo, "thread3");Thread thread4 = new Thread(demo, "thread4");//priority优先级默认是5,最低1,最高10thread1.setPriority(Thread.MAX_PRIORITY);thread2.setPriority(Thread.MAX_PRIORITY);thread3.setPriority(Thread.MIN_PRIORITY);thread4.setPriority(Thread.MIN_PRIORITY);thread1.start();thread2.start();thread3.start();thread4.start();}
}

上述程序并不一定是thread1和thread2抢到的票最多

Thread.yield()的使用

用于暂停当前正在执行的线程,让出CPU的使用权,以允许其他线程执行,但是Thread.yield()并不能保证当前线程立即停止执行,具体的行为取决于线程调度器

Java线程的生命周期

Java语言中线程共有六种状态,分别是:

  • NEW(初始化状态)
  • RUNNABLE(可运行状态+运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态)

在操作系统层面,Java线程中的BLOCKED、WAITING、TIMED_WAITING 是一种状态,即休眠状态,也就是只要Java线程处于这三个状态,那么这个线程永远没有CPU的使用权

image-20240220204626915

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

image-20240220204709578

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

image-20240220204805548

Thread常用的方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入的参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从Running进入Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
  • 具体的实现依赖于系统的任务调度器

join方法

  • 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
public class ThreadJoinDemo {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("t begin");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t finished");}});long start = System.currentTimeMillis();t.start();t.join();System.out.println("执行时间:" + (System.currentTimeMillis() - start));System.out.println("Main finish");}
}

stop方法

stop()方法已经被JDK废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。

stop会释放锁,可能造成数据不一致

Java线程的中断机制
  • Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协同机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程有用完全的自主权,它即可以选择理解停止,也可以选择一段时间后停止,也可以选择压根不停止。

API的使用

  • interrupt():将线程的中断标志设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为false。
public class ThreadInterruptTest {static int i = 0;public static void main(String[] args) {System.out.println("begin");Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {i++;System.out.println(i);// Thread.interrupted() 清除中断标志位   --> 只会输出一个 ==========// Thread.currentThread().isInterrupted() 不会清除中断标志位   -> 会输出十个==========if (Thread.interrupted()) {System.out.println("==========");}if (i == 10) {break;}}}});t1.start();t1.interrupt();}
}

利用中断机制优雅的停止线程

while (!Thread.currentThread.isInterrupted() && more work to do) {do more work
}
public class StopThread implements Runnable {@Overridepublic void run() {int count = 0;while (!Thread.currentThread().isInterrupted() && count < 1000) {System.out.println("count = " + count++);}System.out.println("线程停止 : stop thread");}public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new StopThread());thread.start();Thread.sleep(5);thread.interrupt();}
}

使用中断机制时要注意是否中断标志位被清除的情况

public class StopThread implements Runnable {@Overridepublic void run() {int count = 0;while (!Thread.currentThread().isInterrupted() && count < 1000) {System.out.println("count = " + count++);try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();// 重新设置线程中断状态位trueThread.currentThread().interrupt();}}System.out.println("线程停止 : stop thread");}public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new StopThread());thread.start();Thread.sleep(5);thread.interrupt();}
}

处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置为false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

sleep可以被中断 抛出中断异常 : sleep interrupted,清除中断标志位

wait可以被中断 抛出中断异常 InterruptedException 清除中断标志位

Java线程间通信
  • volatile

volatile有两大特性,一个是可见性,二是有序性,禁止指令重排序,其中可见性就是让线程之间通信

public class VolatileDemo {private static volatile boolean flag = true;public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {while (true){if (flag){System.out.println("trun on");flag = false;}}}}).start();new Thread(new Runnable() {@Overridepublic void run() {while (true){if (!flag){System.out.println("trun off");flag = true;}}}}).start();}
}### 结果trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
  • 等待唤醒(等待通知)机制

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用改线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒

public class WaitDemo {private static Object lock = new Object();private static boolean flag = true;public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock) {while (flag) {try {System.out.println("wait start .....");lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("wait end..........");}}}).start();new Thread(new Runnable() {@Overridepublic void run() {if (flag) {synchronized (lock) {if (flag) {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}lock.notifyAll();System.out.println("notify ......");flag = false;}}}}}).start();}
}

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待”许可“,调用unpark则为指定线程提供”许可“。使用它可以在任何场所使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但是注意连续多次的唤醒效果和一次唤醒是一样的。

public class LockSupportTest {public static void main(String[] args) {Thread parkThread = new Thread(new ParkThread());parkThread.start();System.out.println("唤醒parkThread");// 为指定线程parkThread提供许可LockSupport.unpark(parkThread);}static class ParkThread implements Runnable {@Overridepublic void run() {System.out.println("ParkThread开始执行");// 等待许可LockSupport.park();System.out.println("ParkThread执行完成");}}
}
  • 管道输入输出流

管道输入/输出流和普通文件输入/输出流或网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输媒介为内存。管道输入/输出主要包括了以下四种实现:

PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符

public class PipedTest {public static void main(String[] args) throws Exception {PipedWriter out = new PipedWriter();PipedReader in = new PipedReader();// 将输入流和输出流进行连接,否则在使用时会抛出IOExceptionout.connect(in);Thread printThread = new Thread(new Print(in), "PrintThread");printThread.start();int receive = 0;try {while ((receive = System.in.read()) != -1) {out.write(receive);}} finally {out.close();}}static class Print implements Runnable {private PipedReader in;public Print(PipedReader in) {this.in = in;}@Overridepublic void run() {int receive = 0;try {while ((receive = in.read()) != -1) {System.out.println((char) receive);}} catch (IOException e) {throw new RuntimeException(e);}}}
}
  • Thread.join

join可以理解成线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实是已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现是基于等待通知机制的。

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

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

相关文章

C++:STL简介

1. 什么是STL STL(standard template libaray- 标准模板库 ) &#xff1a; 是 C 标准库的重要组成部分 &#xff0c;不仅是一个可复用的组件库&#xff0c;而且 是一个包罗数据结构与算法的软件框架 。 2. STL的版本 3. STL的六大组件 4.STL的缺陷 1. STL库的更新太慢了。这…

C++ Primer 笔记(总结,摘要,概括)——第1章 开始

目录 1.1 编写一个简单的C程序 1.1.1 编译、运行程序 1.2 初识输入输出 1.3 注释简介 1.4 控制流 1.4.1 while语句 1.4.2 for语句 1.4.3 读取数量不定的输入数据 1.4.4 if语句 1.5 类简介 1.5.1 Sales_item类 1.5.2 初识函数成员 1.6 书店程序 小结 术语表 1.1 编写一个…

0-前置知识

前言 SpringBoot框架在设计之初&#xff0c;为了有更好的兼容性&#xff0c;在不同的运行阶&#xff0c;段提供了非常多的扩展点&#xff0c;可以让程序员根据自己的需求&#xff0c;在整个Spring应用程序运行过程中执行程序员自定义的代码。 ApplicationContextInitializer …

git上传报错:Object too large, rejecting the pack

在gerrit设置了最大不能上传超过600M的文件&#xff0c;今天开发遇到推送问题&#xff1a; 结果到本地怎么也找不到大文件。 后来只能按commit排查&#xff0c;用如下命令排查到了&#xff1a; 解决方法,将大文件去掉&#xff1a;&#xff08;commitid为大文件所在commit&…

《初阶数据结构》尾声

目录 前言&#xff1a; 《快速排序&#xff08;非递归&#xff09;》: 《归并排序》&#xff1a; 《归并排序&#xff08;非递归&#xff09;》&#xff1a; 《计数排序》&#xff1a; 对于快速排序的优化&#xff1a; 分析&#xff1a; 总结&#xff1a; 前言&#xff1a…

linux学习笔记3——文件移动、复制、与搜索命令

对于文件夹操作&#xff0c;linux还支持移动——mv与复制——cp两个命令 mv文件名所要移动目录&#xff0c;具体可以使用.来代表当前目录 cp文件名所要复制目录&#xff0c;二者在目录栏均可对文件重命名 对于find命令&#xff0c;有很多种用法&#xff0c;这里find加所搜索文…

Azkaban【基础 01】核心概念+特点+Web界面+架构+Job类型(一篇即可入门Azkaban工作流调度系统)

Azkaban工作流调度系统 1. 工作流调度系统解决了什么问题2. 特点3. 与Ooize简单对比4. 架构5. Job类型6. 总结 1. 工作流调度系统解决了什么问题 我曾经参与过一个数据治理的项目&#xff0c;项目的大概流程是【数据获取-数据清洗入库-展示】&#xff1a; 这时候就出现问题了&…

【讨论】Web端测试和App端测试的不同,如何说得更有新意?

Web 端测试和 App 端测试是针对不同平台的上的应用进行测试&#xff0c;Web应用和App端的应用实现方式不同&#xff0c;测试时的侧重点也不一样。 Web端应用和App端应用的区别&#xff1a; 平台兼容性 安装方式 功能和性能 用户体验 更新和维护 测试侧重点有何不同 平台…

进程间如何进行通信?

进程间如何进行通信&#xff1f; 本文转自 公众号 ByteByteGo&#xff0c;如有侵权&#xff0c;请联系&#xff0c;立即删除 在 Linux 上&#xff0c;进程之间如何通信&#xff1f; 下图显示了进程间通信的 5 种方式。 01 管道&#xff08;Pipe&#xff09; 管道是单向字节流…

Java入门-可重入锁

可重入锁 什么是可重入锁? 当线程获取某个锁后&#xff0c;还可以继续获取它&#xff0c;可以递归调用&#xff0c;而不会发生死锁&#xff1b; 可重入锁案例 程序可重入加锁 A.class,没有发生死锁。 sychronized锁 package com.wnhz.lock.reentrant;public class Sychroniz…

泽攸科普——扫描电子显微镜(SEM)像衬度形成原理

扫描电镜通过电子束在样品表面进行周而复始的扫描运动&#xff0c;同时实时监测各种信号图像的产生&#xff0c;然后根据产生的信号多少来调制图像。 引起各种信号产生的扫描电镜图像衬度的来源有三个方面&#xff1a; 1. 试样本身性质&#xff1a;包括表面的凹凸不平、成分的…

哪些软件可以把试卷照片转换成电子版?试试这些软件

哪些软件可以把试卷照片转换成电子版&#xff1f;在数字化时代&#xff0c;纸质试卷的保存和传输都显得不太方便。为了解决这个问题&#xff0c;我们可以将试卷照片转换成电子版。下面&#xff0c;我将为大家介绍5款可以轻松实现这一功能的软件&#xff0c;让你轻松应对各种试卷…