当前位置: 首页 > news >正文

JAVA进阶学习14

文章目录

  • 常用工具包commons-io
  • Hutool工具包
  • 一、多线程
    • 1.1 多线程的实现方法
    • 1.2 多线程常见的成员方法
    • 1.3 线程的安全问题
    • 1.4 同步方法
    • 1.5 lock锁
    • 1.6 线程的死锁
    • 1.7 生产者消费者问题(等待唤醒机制)
    • 1.8 阻塞队列——同样解决生产和消费的问题
    • 1.9 线程的状态
  • 二、多线程的其他概念
    • 2.1 线程池
    • 2.2 自定义线程池

常用工具包commons-io

  1. 当导入外部包时通常需要新建一个文件夹,一般命名为lib(libiary)
    在这里插入图片描述

  2. 在lib中一般存储.jar的文件,这种文件相当于java中的压缩包,一般包含了多个类

  3. 放在该文件夹下使用时可以直接导入
    在这里插入图片描述
    4.右键jar文件,将包添加到库中


commons-io是一个集成的IO流,里面有许多现成的IO方法,不用自己去关注底层(用传统的IOf方法实现)

Hutool工具包

国内程序员开发的聚合多功能的工具包
Hutool工具包
在官网中有下载方式和API文档

在该包下有许多已经封装好的函数可以直接使用

一、多线程

多线程的相关概念:

  • 什么是多线程?
    进程:进程是程序执行的基本实体,一个软件的运行通常便伴随着进程的创建
    线程:它是操作系统中运算调度的最小单位,被包含在进程当中,是进程中的实际运作单位
    多线程:在同一个进程中有多个调度同时被执行。例如QQ中可以发消息,打电话,看空间,这每一个功能是一个线程
  • 为什么引入多线程?
    为了提高CPU的工作效率,减少空闲时间,让程序同时做多件事情。
  • 多线程的应用场景
    软件中耗时的操作、聊天软件、服务器

并发和并行:

并发:同一时间有多个指令在CPU上交替执行,类似接力跑
并行:同一时间有多个指令在CPU上同时执行,类似两条赛道上的对手

1.1 多线程的实现方法

1. 继承Thread类,并重写run方法

public class MyThread extends Thread{@Overridepublic void run() {//书写线程要执行代码for (int i = 0; i < 100; i++) {System.out.println(this.getName() + "HelloWorld");}}
}测试类:
public class ThreadDemo {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程1");//为线程设置名字t2.setName("线程2");t1.start();t2.start();}
}

注意:这里使用的是strat方法,不可以直接用run方法

2. 用类实现Runable接口*

public class MyRun implements Runnable{@Overridepublic void run() {//书写线程要执行的代码for (int i = 0; i < 100; i++) {//获取到当前线程的对象/*Thread t = Thread.currentThread();System.out.println(t.getName() + "HelloWorld!");*/System.out.println(Thread.currentThread().getName() + "HelloWorld!");}}
}

public class ThreadDemo {public static void main(String[] args) {/** 多线程的第二种启动方式:*   1.自己定义一个类实现Runnable接口*   2.重写里面的run方法*   3.创建自己的类的对象*   4.创建一个Thread类的对象,并开启线程* *///创建MyRun的对象//表示多线程要执行的任务MyRun mr = new MyRun();//创建线程对象Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);//给线程设置名字t1.setName("线程1");t2.setName("线程2");//开启线程t1.start();t2.start();}
}

注意:

  • 用此方法获取线程名时无法直接使用getName方法了,应用hread.currentThread()获取当前的线程对象再调用线程的方法
  • 此时相当于同一个对象在多个线程中运行,此时的两个线程t1,t2操作的是同一个地址

3. 通过实现Callable接口可以实现带返回值的多线程函数

public class MyCallable implements Callable<Integer> {
//在实现接口时需要申明变量类型@Overridepublic Integer call() throws Exception {//求1~100之间的和int sum = 0;for (int i = 1; i <= 100; i++) {sum = sum + i;}return sum;}
}
public class ThreadDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {/**   多线程的第三种实现方式:*       特点:可以获取到多线程运行的结果**       1. 创建一个类MyCallable实现Callable接口*       2. 重写call (是有返回值的,表示多线程运行的结果)*       3. 创建MyCallable的对象(表示多线程要执行的任务)*       4. 创建FutureTask的对象(作用管理多线程运行的结果)*       5. 创建Thread类的对象,并启动(表示线程)* *///创建MyCallable的对象(表示多线程要执行的任务)MyCallable mc = new MyCallable();//创建FutureTask的对象(作用管理多线程运行的结果)FutureTask<Integer> ft = new FutureTask<>(mc);//创建线程的对象Thread t1 = new Thread(ft);//启动线程t1.start();System.out.println(t1.getName());//获取多线程运行的结果Integer result = ft.get();//从线程管理器中获取结果System.out.println(result);}
}

总结:

  1. 多线程的实现总共分为两类,一种是能返回函数的结果的,另一类是不可以的
  2. thread用于实现简单的多线程程序,因为java的单继承特性导致这个类只能继承于一个类从而失去灵活性,而Runable接口的方法相对灵活,一个类可以实现多个接口
  3. 方式2、3都是将要运行的程序放在线程当中,但方式1可以直接作为线程使用

1.2 多线程常见的成员方法

在这里插入图片描述

  • 前四个方法:
线程函数的定义:
public class MyThread extends Thread{public MyThread() {}public MyThread(String name) {super(name);}@Overridepublic void run() {for (int i = 0; i < 100; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + "@" + i);}}
}
线程测试:
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {String getName()                    返回此线程的名称void setName(String name)           设置线程的名字(构造方法也可以设置名字)细节:1、如果我们没有给线程设置名字,线程也是有默认的名字的格式:Thread-XX序号,从0开始的)2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置static Thread currentThread()       获取当前线程的对象细节:当JVM虚拟机启动之后,会自动的启动多条线程其中有一条线程就叫做main线程他的作用就是去调用main方法,并执行里面的代码在以前,我们写的所有的代码,其实都是运行在main线程当中static void sleep(long time)        让线程休眠指定的时间,单位为毫秒细节:1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间2、方法的参数:就表示睡眠的时间,单位毫秒1= 1000毫秒3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码//1.创建线程的对象MyThread t1 = new MyThread("线程1号");MyThread t2 = new MyThread("线程2号");//2.开启线程t1.start();t2.start();//哪条线程执行到这个方法,此时获取的就是哪条线程的对象Thread t = Thread.currentThread();String name = t.getName();System.out.println(name);//main*///获取的main线程,不受1、2线程的影响System.out.println("11111111111");Thread.sleep(5000);System.out.println("22222222222");}
}

注意:在重写run方法是不能抛出异常,因为其继承的父类也没有抛出异常

  • 线程优先级相关方法:

线程的优先级就是线程运行的优先顺序,优先级最大是10,最小是1,默认为5.

public class ThreadDemo {public static void main(String[] args){/*setPriority(int newPriority)        设置线程的优先级final int getPriority()             获取线程的优先级*///创建线程要执行的参数对象MyRunnable mr = new MyRunnable();//创建线程对象Thread t1 = new Thread(mr,"线程1");//在线程中加载对象Thread t2 = new Thread(mr,"线程2");t1.setPriority(1);t2.setPriority(10);t1.start();t2.start();}
}

注意优先级高只是抢占上资源的概率大,并不一定能长时间执行

  • 守护线程
public class ThreadDemo {public static void main(String[] args) {/*final void setDaemon(boolean on)    设置为守护线程细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束         */MyThread1 t1 = new MyThread1();MyThread2 t2 = new MyThread2();t1.setName("非守护线程");t2.setName("守护线程");//把第二个线程设置为守护线程(备胎线程)t2.setDaemon(true);t1.start();t2.start();}
}

总结:守护线程伴随着所有非守护线程的结束而结束

  • 礼让线程(了解)
    在这里插入图片描述

该方法是一个静态方法,可直接调用
会出让CPU的执行权让线程的运行尽可能的均匀

public class MyThread extends Thread{@Overridepublic void run() {for (int i = 1; i <= 100; i++) {System.out.println(getName() + "@" + i);//表示出让当前CPU的执行权Thread.yield();}}
}
  • 插队线程(了解)
    在这里插入图片描述
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {/*public final void join()  插入线程/插队线程*///当前线程: main线程MyThread t = new MyThread();MyThread t2 =new MyThread();t2.setName("线程1");t.setName("线程2");t.start();//t.join();t2.start();t2.join();//会占用当前的线程//执行在main线程当中的for (int i = 0; i < 100; i++) {System.out.println("main线程" + i);}}
}

1.3 线程的安全问题

在制作一个多线程的抢票程序时

  • 多个线程需要共享一个剩余票数的变量,设置类的静态变量
  • 线程在同时执行时可能同时会有多个线程跨国判断导致变量错误计数,此时需要进程死锁,保证每次只能有一个进程进入被锁的代码块(同步代码块)
public class MyThread extends Thread {//表示这个类所有的对象,都共享ticket数据static int ticket = 0;//0 ~ 99@Overridepublic void run() {while (true) {//同步代码块synchronized (MyThread.class) { //此处的锁函数中需要传入锁对象,对象可以是任意的但一定要保证唯一性if (ticket < 100) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票!!!");} else {break;}}}}

注意

  1. 同步代码块一定要写在循环的内部,这样可以保证多个线程各自监听,一有机会就进入同步代码块执行抢票程序段
  2. 一般用当前类的字节码文件:MyThread.class来充当锁对象

1.4 同步方法

  • 当一整个方法都被包含在同步代码块内部时,可以考虑使用同步方法,可以有更好的封装性。
  • 同步方法一般都是在同步代码块的基础上改进而来的
  • 同步方法的格式在这里插入图片描述

在这里插入图片描述

选中同步代码块部分按Ctrl+Alt+M可以直接将同步代码块抽象成同步方法


下面用Runable方法举例:

public class MyRunnable implements Runnable {int ticket = 0;@Overridepublic void run() {//1.循环while (true) {//2.同步代码块(同步方法)if (method()) break;}}//this此处并没有用静态,表示锁对象是thisprivate synchronized boolean method() {//3.判断共享数据是否到了末尾,如果到了末尾if (ticket == 100) {return true;} else {//4.判断共享数据是否到了末尾,如果没有到末尾try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");}return false;}
}
public class ThreadDemo {public static void main(String[] args) {/*需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票利用同步方法完成技巧:同步代码块*/MyRunnable mr = new MyRunnable();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);Thread t3 = new Thread(mr);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}

注意
因为Runable实现方法的特点,所有的线程共享一个对象,因此在变量定义时不用使用静态变量,并且在申明同步方法时也不用静态方法(此时为this)

1.5 lock锁

为了解决同步代码块在运行过程中不能手动上锁解锁的困境,在JDK5以后可以使用lock锁对代码块上锁和解锁。
Lock类是一个接口,一般会使用它的实现类ReentrantLock

public class MyThread extends Thread{static int ticket = 0;static Lock lock = new ReentrantLock();@Overridepublic void run() {//1.循环while(true){//2.同步代码块//synchronized (MyThread.class){lock.lock(); //2 //3try {//3.判断if(ticket == 100){break;//4.判断}else{Thread.sleep(10);ticket++;System.out.println(getName() + "在卖第" + ticket + "张票!!!");}//  }} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}
}
public class ThreadDemo {public static void main(String[] args) {/*需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票*/MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}

注意

  • 因为三个线程对象共用一把锁,此时应该设置锁为静态
  • 如果只是在之前的方法头尾改为上锁和解锁的话,会导致某个线程在break跳出循环后并未解锁,而其他的线程还在等锁解开,从而导致程序无法终止。
    在这里插入图片描述
    为了解决这个问题,将同步代码块的部分直接包裹在try…catch……finally语句中,致使每次运行结束后都能运行unlock()语句
    快捷键:Ctrl+Alt+T可语句体包裹

1.6 线程的死锁

这是一个线程问题中由于锁嵌套产生的意外情况
每个线程都占有着一部分资源,但是他们都需要更多的资源才能继续向下进行,导致所有的线程都停滞不动,并且一直占有着资源
如有一双筷子两个人用,他们每人抢到一支,但是都没办法吃饭

1.7 生产者消费者问题(等待唤醒机制)

这个模型较为理想化地展示了多线程之间的需求关系,生产者生产资料等消费者使用,用后再生产
在这里插入图片描述

为了方便理解用了厨师和食客的例子展示:
在这里插入图片描述
分析:

  • 该程序分为三个类,两个线程。厨师和食客分别作为一个类和线程,桌子作为中间类用来调控两个类的进行
  • 食客吃完就进入等待状态暂时不再参与锁的竞争,等厨师做好饭后将其唤醒继续参与锁竞争
  • 同理,厨师做好饭也进入等待状态暂时不参与锁的竞争,等食客吃完后才被唤醒

厨师部分

public class Cook extends Thread{@Overridepublic void run() {/** 1. 循环* 2. 同步代码块* 3. 判断共享数据是否到了末尾(到了末尾)* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)* */while (true){synchronized (Desk.lock){if(Desk.count == 0){ break;}else{//判断桌子上是否有食物if(Desk.foodFlag == 1){//如果有,就等待try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}else{//如果没有,就制作食物System.out.println("厨师做了一碗面条");//修改桌子上的食物状态Desk.foodFlag = 1;//叫醒等待的消费者开吃Desk.lock.notifyAll();}}}}}
}

食客部分

public class Foodie extends Thread{@Overridepublic void run() {/** 1. 循环* 2. 同步代码块* 3. 判断共享数据是否到了末尾(到了末尾)* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)* */while(true){synchronized (Desk.lock){if(Desk.count == 0){break;}else{//先判断桌子上是否有面条if(Desk.foodFlag == 0){//如果没有,就等待try {Desk.lock.wait();//让当前线程跟锁进行绑定} catch (InterruptedException e) {e.printStackTrace();}}else{//把吃的总数-1Desk.count--;//如果有,就开吃System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");//吃完之后,唤醒厨师继续做Desk.lock.notifyAll();//修改桌子的状态Desk.foodFlag = 0;}}}}}
}

第三方控制类(桌子)

public class Desk {/** 作用:控制生产者和消费者的执行* *///是否有面条  0:没有面条  1:有面条public static int foodFlag = 0;//总个数public static int count = 10;//锁对象,必须是唯一的public static Object lock = new Object();
}

在这部分定义了锁对象和共享变量


总结:

  • 线程中一般在循环中嵌套同步代码块
  • 用锁对象来调用等待和唤醒的方法

1.8 阻塞队列——同样解决生产和消费的问题

队列:就是我们熟知的队列结构
阻塞:队列中的产品装满或者为空时会产生阻塞现象
在这里插入图片描述

食客:

public class Foodie extends Thread{ArrayBlockingQueue<String> queue;//带队列的构造方法public Foodie(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while(true){//不断从阻塞队列中获取面条try {String food = queue.take();System.out.println(food);} catch (InterruptedException e) {e.printStackTrace();}}}
}

厨师:

public class Cook extends Thread{ArrayBlockingQueue<String> queue;//带队列的构造方法public Cook(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while(true){//不断的把面条放到阻塞队列当中try {queue.put("面条");System.out.println("厨师放了一碗面条");} catch (InterruptedException e) {e.printStackTrace();}}}
}

测试类:

public class ThreadDemo {public static void main(String[] args) {/***    需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码*    细节:*           生产者和消费者必须使用同一个阻塞队列** *///1.创建阻塞队列的对象,给出队列中的存储容量ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);//2.创建线程的对象,并把阻塞队列传递过去Cook c = new Cook(queue);Foodie f = new Foodie(queue);//3.开启线程c.start();f.start();}
}

总结

  • 生产者和消费者共用同一队列,可以在线程初始化的时候将其传入
  • 队列在定义时可以定义存储的大小
  • 队列的put和take方法是自带同步代码块的,不用再将其嵌套在同步代码块中,否则可能会产生死锁现象

1.9 线程的状态

在这里插入图片描述
但是在JAVA中并没有定义线程的运行状态,此时线程是交由操作系统处理的,并不是虚拟机管理。

二、多线程的其他概念

2.1 线程池

线程在运行结束后会被清除,线程池的主要目的是为了避免多次创建同一个线程而消耗系统资源,当有同样的任务等待处理时可以直接调用上次创建的线程

线程池的实现
在这里插入图片描述

public class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 1; i <= 100; i++) {System.out.println(Thread.currentThread().getName() + "---" + i);}}
}
public class MyThreadPoolDemo {public static void main(String[] args) throws InterruptedException {/*public static ExecutorService newCachedThreadPool()             创建一个没有上限的线程池public static ExecutorService newFixedThreadPool (int nThreads) 创建有上限的线程池*///1.获取线程池对象//ExecutorService pool=Executors.newCachedThreadPool();//创建没有上限的线程池ExecutorService pool1 = Executors.newFixedThreadPool(3);//2.提交任务,向线程池中提交任务pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());//3.销毁线程池//pool1.shutdown();}
}

下面的图片是线程池运行过程中的具体情况,可以看见当前活跃的线程和排队等待的线程数
在这里插入图片描述

2.2 自定义线程池

线程池的运行策略

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列(阻塞队列)里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程(临时线程)来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

用ThreadPoolExecutor实现
其总共可以设置7个参数

  1. corePoolSize:核心线程数,线程池中始终存活的线程数。

  2. maximumPoolSize:最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。

  3. keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。

  4. unit:单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间。参数 keepAliveTime 的时间单位有以下 7 种可选:

    • TimeUnit.DAYS:天
    • TimeUnit.HOURS:小时
    • TimeUnit.MINUTES:分
    • TimeUnit.SECONDS:秒
    • TimeUnit.MILLISECONDS:毫秒
    • TimeUnit.MICROSECONDS:微妙
    • TimeUnit.NANOSECONDS:纳秒
  5. workQueue:一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全。它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种,包含以下 7 种类型:

    • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
    • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
    • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关

  1. threadFactory:线程工厂,主要用来创建线程。

  2. handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

    • AbortPolicy:拒绝并抛出异常。
    • CallerRunsPolicy:使用当前调用的线程来执行此任务。
    • DiscardOldestPolicy:抛弃任务队列头部(最旧)的一个任务,并执行当前任务。
    • DiscardPolicy:忽略并抛弃当前任务。
 ThreadPoolExecutor pool = new ThreadPoolExecutor(3,  //核心线程数量,能小于06,  //最大线程数,不能小于0,最大数量 >= 核心线程数量60,//空闲线程最大存活时间TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue<>(3),//任务队列Executors.defaultThreadFactory(),//创建线程工厂new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略);

解析:

  • 为什么要设置阻塞队列
    因为线程的销毁和创建都很消耗系统的资源,涉及到系统底层的资源分配回收机制,所以不到不得已不会创建临时线程来解决超出等待队列的任务。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • RuoYi-Cloud 部署与配置 [CentOS7]
  • 《深入浅出WPF》读书笔记.8路由事件
  • 使用pgrs在wsl中为postgres写拓展
  • huggingface.co 无法访问问题换源解决
  • c++修炼之路之C++11
  • Mac/Linux系统matplotlib中文支持问题
  • Java中类的成员介绍
  • 设计模式-结构性模式
  • Elasticsearch 里的父子文档插入和查询
  • upload-labs通关攻略
  • jetson orin nx安装todesk
  • Matlab三维图的坐标轴标签 自动平行坐标/自动旋转
  • Android耗电优化,如何定位问题,如何修改
  • Unity学习路线
  • vscode上传自己开发的npm包
  • Git 使用集
  • Java 最常见的 200+ 面试题:面试必备
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • javascript 总结(常用工具类的封装)
  • Java精华积累:初学者都应该搞懂的问题
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • log4j2输出到kafka
  • passportjs 源码分析
  • PHP 的 SAPI 是个什么东西
  • 从零开始的无人驾驶 1
  • 目录与文件属性:编写ls
  • 排序(1):冒泡排序
  • 配置 PM2 实现代码自动发布
  • 扑朔迷离的属性和特性【彻底弄清】
  • 使用 5W1H 写出高可读的 Git Commit Message
  • 携程小程序初体验
  • 要让cordova项目适配iphoneX + ios11.4,总共要几步?三步
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • ​浅谈 Linux 中的 core dump 分析方法
  • #QT(QCharts绘制曲线)
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • $HTTP_POST_VARS['']和$_POST['']的区别
  • (16)UiBot:智能化软件机器人(以头歌抓取课程数据为例)
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (附源码)ssm考生评分系统 毕业设计 071114
  • (四)opengl函数加载和错误处理
  • (一)Thymeleaf用法——Thymeleaf简介
  • (转)ObjectiveC 深浅拷贝学习
  • (转)socket Aio demo
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET 5.0正式发布,有什么功能特性(翻译)
  • .NET8 动态添加定时任务(CRON Expression, Whatever)
  • /deep/和 >>>以及 ::v-deep 三者的区别
  • ??myeclipse+tomcat
  • @vue/cli脚手架
  • [20170705]diff比较执行结果的内容.txt
  • [acwing周赛复盘] 第 94 场周赛20230311
  • [Bug]使用gradio创建应用提示AttributeError: module ‘gradio‘ has no attribute ‘inputs‘
  • [BZOJ1178][Apio2009]CONVENTION会议中心