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

Java学习笔记-多线程基础

15. 多线程基础

15.1 线程相关概念

  1. 程序(program)

    是为了完成特定任务,用某种语言编写的一组指令的集合,简单的说就是我们的代码

  2. 进程

    1. 进程是指运行中的程序,比如我们使用的QQ双击QQ.exe文件便启动了一个进程,操作系统会为该进程分配内存空间,又如我们写了一段代码,然后编译运行,也是启动了一个进程

    2. 进程是程序的一次执行,或是正在运行的一个程序,是动态过程:有它自身的产生,存在和消亡的过程

      在这里插入图片描述

  3. 线程

    1. 线程由进程创建,是进程的一个实体,如下载和上传,可以同时下载多个文件,这就产生了多个线程,又如QQ聊天时的多个窗口,你可以跟A同学聊天,同时又跟B同学聊天,也是线程

    2. 一个进程可以有多个线程,便有单线程和多线程的概念,如下图

      • 单线程:同一时刻,只能执行一个线程

      • 多线程:同一时刻,可以执行多个线程,比如一个QQ进程可以打开多个聊天窗口

      在这里插入图片描述

  4. 并发

    1. 同一时刻,多个任务交替执行,造成一种 “貌似同时” 的错觉,简单的说就是单核CPU实现的多任务就是并发

    2. 如下图所示,单核CPU,它要执行两个任务,因此它只能一会执行QQ,一会执行迅雷这样交替进行,就是并发,也可以比作人的大脑,边开车边打电话就是并发

      在这里插入图片描述

  5. 并行

    1. 同一时刻,多个任务同时执行,多核CPU可以实现并行,或者并发和并行

    2. 例如下图,对于双核CPU,如果再开一个进程,比如说微信,对于上面的CPU可能就是并发的(交替)执行QQ和微信,但是对于CPU执行QQ和迅雷来说就是并行的

      在这里插入图片描述

15.2 线程基本使用

15.2.1 创建线程的方式

在Java中创建线程的方式有两种

  1. 继承Thread类,重写run方法(Thread意思即线程)

    class Dog extends Thread {@Overridepublic void run() {//super.run();  一般不调用父类的}
    }
    
  2. 实现Runnable接口,重写run方法,其实Thread类也是实现了Runnable接口

    class Dog implements Runnable {@Overridepublic void run() {   }
    }
    

    在这里插入图片描述

15.2.2 继承Thread类

在这里插入图片描述

步骤(以下面的代码为基础理解):

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

  2. 创建一个该类的对象,此时便可以当做线程来使用

  3. cat.start方法用来启动线程,内部代码会调用start0()这个方法,然后再在start0()这个方法中以特殊的方式调用run方法,若直接调用 run 只是普通的方法调用而已

    • start0() 是本地方法,是JVM调用, 底层是c/c++实现

      private native void start0();
      
    • 真正实现多线程的效果, 是start0(), 而不是run

    在这里插入图片描述

说明

  1. 当一个类继承了 Thread 类, 该类就可以当做线程使用

  2. 我们会重写 run方法,写上自己的业务代码

  3. run Thread 类 实现了 Runnable 接口的run方法

  4. 当主线程结束了,子线程不会结束,会继续执行,且子线程中可以再创建线程

阅读代码,注释及执行结果理解线程的使用:

public class Main {public static void main(String[] args) throws InterruptedException {// 创建Cat对象,可以当做线程使用Cat cat = new Cat();cat.start();   // 启动线程 -> 最终会执行cat的run方法// 理解线程:执行cat.start之后,开启另一个线程,原本只有一个线程// 即main函数这里这个主线程,开启线程后,下面的代码将会和run里的代码// 会交替执行,即主线程和子线程交替执行// cat.run();//run方法就是一个普通的方法,如果只是这样普通调用run方法// 没有真正的启动一个线程,就会把run方法执行完毕才向下执行,因此不直接调用run方法// 说明: 当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行// 这时 主线程和子线程是交替执行..for(int i = 1; i<= 5; i++) {// Thread.currentThread().getName() 获取当前线程的名字Thread.sleep(100);   //休眠100msSystem.out.println(Thread.currentThread().getName() + "主线程" + i);}}
}class Cat extends Thread {int count = 0;@Overridepublic void run() {while(true) {if(count == 5) break;System.out.println(Thread.currentThread().getName() +"子线程:小猫喵喵叫..." + (++count));try {Thread.sleep(100);   //线程休眠100ms,1000ms=1s} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码执行结果:

在这里插入图片描述

在这里插入图片描述

15.2.3 实现Runnable接口

  1. 基本介绍

    1. java 是单继承机制,在某些的情况下一个类可能已经继承了另一个类,这时便不能在继承Thread类了,用继承Thread的方法创建线程就不可能了
    2. javad设计者便提供了另一种方式创建线程,就是通过实现Runnable接口来实现线程
  2. 举例讲解

    在这里插入图片描述

    1. 写法一:先创建一个dog,在new一个Thread,然后在调用Threadstart函数

      //这里用到了静态代理模式
      public class Thread02 {public static void main(String[] args) {Dog dog = new Dog();//dog.start(); 这里不能调用start//创建了Thread对象,把 dog对象(实现Runnable),放入ThreadThread thread = new Thread(dog);thread.start();}
      }class Dog implements Runnable { //通过实现Runnable接口,开发线程int count = 0;@Overridepublic void run() { //普通方法//写自己的业务逻辑代码}
      }
      
    2. 方法二:直接在类中维护一个私有的Thread对象,然后写一个start(注意这不是重写),在方法中初始化该Thread对象,然后调用它的start方法

      public class test {public static void main(String[] args) {Dog dog = new Dog();dog.start();for(int i=1;i<1000;i++) {System.out.println("000");}// Thread类的start方法只能调用一次,如果下面的t.start放外面了的话// 那这里第二次调用dog.start就会报错
      //        dog.start();  }
      }class Dog implements Runnable {private Thread t;   //维护一个私有的Thread对象@Overridepublic void run() {for(int i=0;i<10;i++) {System.out.println("1");}}public void start() {if(t==null) {t = new Thread(this);t.start();   //注意这个是放里面,而不是外面,保证start方法只被调用一次}}
      }

    静态代理设计模式的简单理解:

    public class Thread02 {public static void main(String[] args) {Tiger tiger = new Tiger();//实现了 RunnableThreadProxy threadProxy = new ThreadProxy(tiger);threadProxy.start();}
    }//线程代理类 , 模拟了一个极简的Thread类
    class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxyprivate Runnable target = null;//属性,类型是 Runnable@Overridepublic void run() {if (target != null) {target.run();//动态绑定(运行类型Tiger)}}public ThreadProxy(Runnable target) {this.target = target;}public void start() {start0();//这个方法时真正实现多线程方法}public void start0() {run();}
    }
    

15.2.4 JConsole监控线程

15.2.2继承Thread类中的代码为例

首先将循环结束的条件改大点或休眠时间长点,避免还没打开JConsole线程已经执行完了

步骤

  1. 开始运行程序后,点击Terminal(终端)

    在这里插入图片描述

  2. 输入JConsole(不分大小写)回车,便会进入Java监视和管理控制台窗口,然后再窗口中选择本地进程,便会在里面看见当前进程即com.qingtian.demo1.Main(包名+类名),选择后点击连接即可

    在这里插入图片描述

  3. 连接中会出现这个情况,直接不安全连接即可

    在这里插入图片描述

  4. 之后在左上角选择线程,然后在左下角可以找到main和Thread-0,即两个线程的名字

    在这里插入图片描述

    在这里插入图片描述

  5. 当main线程执行结束时,此时便只剩下Thread-0在执行

    在这里插入图片描述

    在这里插入图片描述

  6. 最后Thread-0也执行完,之后便失去连接,并且连不上了

    在这里插入图片描述

15.2.5 Thread 与 Runnable

  1. 多线程的理解

    在这里插入图片描述

  2. 继承 Thread vs 实现 Runnable 的区别

    在这里插入图片描述

  3. 两者面临的问题:线程同步问题

    在这里插入图片描述

    public class SellTicket {public static void main(String[] args) {//测试
    //        SellTicket01 sellTicket01 = new SellTicket01();
    //        SellTicket01 sellTicket02 = new SellTicket01();
    //        SellTicket01 sellTicket03 = new SellTicket01();
    //
    //        //这里我们会出现超卖..
    //        sellTicket01.start();//启动售票线程
    //        sellTicket02.start();//启动售票线程
    //        sellTicket03.start();//启动售票线程//        //这里也会出现超卖..System.out.println("===使用实现接口方式来售票=====");SellTicket02 sellTicket02 = new SellTicket02();new Thread(sellTicket02).start();//第1个线程-窗口new Thread(sellTicket02).start();//第2个线程-窗口new Thread(sellTicket02).start();//第3个线程-窗口}
    }//使用Thread方式
    class SellTicket01 extends Thread {private static int ticketNum = 100;//让多个线程共享 ticketNum@Overridepublic void run() {while (true) {if (ticketNum <= 0) {System.out.println("售票结束...");break;}//休眠50毫秒, 模拟try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"+ " 剩余票数=" + (--ticketNum));}}
    }//实现接口方式
    class SellTicket02 implements Runnable {private int ticketNum = 100;//让多个线程共享 ticketNum@Overridepublic void run() {while (true) {if (ticketNum <= 0) {System.out.println("售票结束...");break;}//休眠50毫秒, 模拟try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"+ " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2}}
    }
    

15.2.6 线程的终止

  1. 基本说明

    1. 当线程完成任务后,会自动退出
    2. 还可以通过使用变量来控制run方法退出的方式来停止线程,即通知方式
  2. 举例说明

    public class ThreadExit_ {public static void main(String[] args) throws InterruptedException {T t1 = new T();t1.start();//如果希望main线程去控制t1 线程的终止, 必须可以修改 loop//让t1 退出run方法,从而终止 t1线程 -> 通知方式//让主线程休眠 10 秒,再通知 t1线程退出System.out.println("main线程休眠10s...");Thread.sleep(10 * 1000);t1.setLoop(false);}
    }class T extends Thread {private int count = 0;//设置一个控制变量private boolean loop = true;@Overridepublic void run() {while (loop) {try {Thread.sleep(50);// 让当前线程休眠50ms} catch (InterruptedException e) {e.printStackTrace();}System.out.println("T 运行中...." + (++count));}}public void setLoop(boolean loop) {this.loop = loop;}
    }
    

15.3 线程常用方法

15.3.1 常用方法第一组

  1. 基本介绍

    方法作用
    setName设置线程名称
    getName返回该线程名称
    start使该线程开始执行,java虚拟机底层调用该线程的start0方法
    run调用线程对象的run方法
    setPriority更改线程的优先级,优先级有三个常量,可以自己看源码
    getPriority获取线程的优先级
    sleep在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
    interrupt中断休眠,提前结束sleep休眠,注意不是终止线程
  2. 注意细节

    1. start底层会创建新的线程,调用runrun就是一个简单的方法调用,不会启动新的线程

    2. 线程优先级的范围

      在这里插入图片描述

    3. interrupt结束线程的休眠,相对于唤醒

    4. sleep:线程的静态方法,使当前线程休眠

  3. 举例说明

    public class ThreadMethod01 {public static void main(String[] args) throws InterruptedException {//测试相关的方法T t = new T();t.setName("老韩");t.setPriority(Thread.MIN_PRIORITY);//1t.start();//启动子线程//主线程打印5 hi ,然后我就中断 子线程的休眠for(int i = 0; i < 5; i++) {Thread.sleep(1000);System.out.println("hi " + i);}System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1t.interrupt();//当执行到这里,就会中断 t线程的休眠.}
    }class T extends Thread { //自定义的线程类@Overridepublic void run() {while (true) {for (int i = 0; i < 100; i++) {//Thread.currentThread().getName() 获取当前线程的名称System.out.println(Thread.currentThread().getName() + "吃包子~~~~" + i);}try {System.out.println(Thread.currentThread().getName() + "休眠中~~~");// 这里会休眠20秒,但是被main里面的interrupt,便抛出一个异常,之后便执行catchThread.sleep(20000);//20秒} catch (InterruptedException e) {//当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码//InterruptedException 是捕获到一个中断异常.System.out.println(Thread.currentThread().getName() + "被 interrupt了");}}}
    }
    

15.3.2 常用方法第二组

  1. 基本介绍

    1. yield:线程的礼让,让出cpu,让其线程执行,但礼让的时间的不确定,所以也不一定礼让成功

      在这里插入图片描述

    2. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有任务,在执行被插队的线程,注意join 方法是在其他线程中调用,如线程t1调用线程t2的join的方法,意思就是先把t1线程占用的CPU让给t2先执行完,再执行t1

      在这里插入图片描述

      在这里插入图片描述

      public class ThreadMethod02 {public static void main(String[] args) throws InterruptedException {T2 t2 = new T2();t2.start();for(int i = 1; i <= 20; i++) {Thread.sleep(1000);System.out.println("主线程(小弟) 吃了 " + i  + " 包子");if(i == 5) {System.out.println("主线程(小弟) 让 子线程(老大) 先吃");//join, 线程插队,插队成功后先执行t2的线程在执行这里的//t2.join();// 这里相当于让t2 线程先执行完毕Thread.yield();//礼让,不一定成功..System.out.println("线程(老大) 吃完了 主线程(小弟) 接着吃..");}}}
      }class T2 extends Thread {@Overridepublic void run() {for (int i = 1; i <= 20; i++) {try {Thread.sleep(1000);//休眠1秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("子线程(老大) 吃了 " + i +  " 包子");}}
      }
      

15.3.3 用户线程和守护线程

  1. 基本介绍

    1. 用户线程:也叫工作线程,当线程的任务执行完通知方式介绍

    2. 守护线程:一般是为工作线程服务,当所有的用户线程结束,守护线程自动结束

      常见的守护线程:垃圾回收机制

  2. 举例讲解

    public class ThreadMethod03 {public static void main(String[] args) throws InterruptedException {MyDaemonThread myDaemonThread = new MyDaemonThread();//如果我们希望当main线程结束后,子线程自动结束//,只需将子线程设为守护线程即可myDaemonThread.setDaemon(true);  //将这个线程设置成守护线程myDaemonThread.start();//main线程,设置守护线程后,若下面的执行玩了,那子线程即无限for循环也会结束for( int i = 1; i <= 10; i++) {System.out.println("宝强在辛苦的工作...");Thread.sleep(1000);}}
    }class MyDaemonThread extends Thread {public void run() {for (; ; ) {//无限循环try {Thread.sleep(1000);//休眠1000毫秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("马蓉和宋喆快乐聊天,哈哈哈~~~");}}
    }
    

15.4 线程的生命周期

  1. 基本介绍

    在这里插入图片描述

  2. 线程状态转换图

    在这里插入图片描述

    解释说明:

    1. 新new的一个对象,还没有调用start方法前处于New状态
    2. 调用的start的线程处于Runnable状态,其中Runnable状态又可以细分为Ready(准备状态)和Running状态(运行状态)
    3. Runnable状态的线程调用Thread.sleep()或者其他方法则该线程将会进入TimeWaiting状态
    4. Runnable状态的线程调用自身的wait()方法或者在自身线程里调用其他线程的join()方法,就会进入Waiting状态
    5. 等待进入同步代码块,即等待对象锁时,就会处于Blocked状态(堵塞状态)
    6. 线程结束后,便进入了Terminated状态
  3. 举例查看线程状态图

    public class ThreadState_ {public static void main(String[] args) throws InterruptedException {T t = new T();System.out.println(t.getName() + " 状态 " + t.getState());t.start();while (Thread.State.TERMINATED != t.getState()) {System.out.println(t.getName() + " 状态 " + t.getState());Thread.sleep(500);}System.out.println(t.getName() + " 状态 " + t.getState());}
    }	class T extends Thread {@Overridepublic void run() {for (int i = 0; i < 2; i++) {System.out.println("hi " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
    }
    

    在这里插入图片描述

15.5 线程的同步

  1. 基本介绍

    解决15.2.5中多卖出了票的问题

    1. 在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就需要使用同步访问技术保证数据在任何一时刻,最多有一个线程访问,以保证数据的完整性

    2. 理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作

  2. 同步具体方法-Synchronized

    1. 同步代码块

      synchronized(对象) {  //得到对象锁,才能操作同步代码需要被同步的代码
      }
      
    2. synchronized还可以放在方法声明中,表示整个方法为同步方法

      public synchronized void m(String name) {需要被同步的代码
      }
      

      在这里插入图片描述

  3. 分析同步原理

    在这里插入图片描述

15.6 互斥锁

  1. 基本介绍

    1. Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
    2. 每个对象都对应与一个可称为 “互斥锁” 的标记,这个标记用来保证在任一时刻,只能一个线程访问该对象
    3. 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任意时刻只能由一个线程访问
    4. 同步的局限性:导致程序的执行效率降低
    5. 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象
    6. 同步方法(静态的)的锁为当前类本身
  2. 举例讲解

    使用互斥锁来解决售票问题

    public class SellTicket {public static void main(String[] args) {//测试一把,都是用同一对象创建的三个线程,因此下面的对象锁object也是同一个SellTicket03 sellTicket03 = new SellTicket03();new Thread(sellTicket03).start();//第1个线程-窗口new Thread(sellTicket03).start();//第2个线程-窗口new Thread(sellTicket03).start();//第3个线程-窗口}
    }//实现接口方式, 使用synchronized实现线程同步
    class SellTicket03 implements Runnable {private int ticketNum = 100;//让多个线程共享 ticketNumprivate boolean loop = true;//控制run方法变量Object object = new Object();//同步方法(静态的)的锁为当前类本身//老韩解读//1. public synchronized static void m1() {} 锁是加在 SellTicket03.class//2. 如果在静态方法中,实现一个同步代码块./*synchronized (SellTicket03.class) {System.out.println("m2");}*/public synchronized static void m1() {}public static  void m2() {synchronized (SellTicket03.class) {System.out.println("m2");}}//老韩说明//1. public synchronized void sell() {} 就是一个同步方法//2. 这时锁在 this对象//3. 也可以在代码块上写 synchronize ,同步代码块, 互斥锁还是在this对象public /*synchronized*/ void sell() { //同步方法, 在同一时刻, 只能有一个线程来执行sell方法// 因为是同一个对象,所有成员变量object也是同一个对象synchronized (/*this*/ object) {if (ticketNum <= 0) {System.out.println("售票结束...");loop = false;return;}//休眠50毫秒, 模拟try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"+ " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2}}@Overridepublic void run() {while (loop) {sell();//sell方法是一共同步方法}}
    }
    
  3. 注意事项

    1. 同步方法如果没有使用static修饰:默认锁对象为this
    2. 如果方法使用static修饰,默认锁对象:当前类.class
    3. 实现的步骤:
      • 需要先分析上锁的代码
      • 选择同步代码块或同步方法
      • 要求多个线程锁对象为同一个即可

15.7 线程的死锁

  1. 基本介绍

    多个线程都占用了对方的锁资源,但是不肯相让,导致了死锁,在编程是一定要避免死锁发生的

    在这里插入图片描述

  2. 举例讲解

    public class DeadLock_ {public static void main(String[] args) {//模拟死锁现象DeadLockDemo A = new DeadLockDemo(true);A.setName("A线程");DeadLockDemo B = new DeadLockDemo(false);B.setName("B线程");A.start();B.start();}
    }//线程
    class DeadLockDemo extends Thread {static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用staticstatic Object o2 = new Object();boolean flag;public DeadLockDemo(boolean flag) {//构造器this.flag = flag;}@Overridepublic void run() {//下面业务逻辑的分析//1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁//2. 如果线程A 得不到 o2 对象锁,就会Blocked//3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁//4. 如果线程B 得不到 o1 对象锁,就会Blockedif (flag) {synchronized (o1) {//对象互斥锁, 下面就是同步代码System.out.println(Thread.currentThread().getName() + " 进入1");synchronized (o2) { // 这里获得li对象的监视权System.out.println(Thread.currentThread().getName() + " 进入2");}}} else {synchronized (o2) {System.out.println(Thread.currentThread().getName() + " 进入3");synchronized (o1) { // 这里获得li对象的监视权System.out.println(Thread.currentThread().getName() + " 进入4");}}}}
    }
    

15.8 释放锁

  1. 下面操作会释放锁

    1. 当前线程的同步方法,同步代码块执行结束

      类比:上厕所,完事出来

    2. 当前线程在同步代码块,同步方法中遇到breakreturn

      类比:没有正常完事,经理叫他修改bug,不得已出来

    3. 当前线程在同步代码块,同步方法中出现了未处理的ErrorException,导致异常结束

      类比:没有正常完事,发现忘带纸,不得已出来

    4. 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

      类比:没有正常完事,觉得需要酝酿,所以出来等会再进去

  2. 下面操作不会释放锁

    1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行,不会释放锁

      类比:上厕所,太困了,在坑位上眯了会

    2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁

      注意:应尽量避免使用suspend()resume()来控制线程,方法不在推荐使用

http://www.xdnf.cn/news/19383.html

相关文章:

  • Kafka 4.0 生产者配置全解析与实战调优
  • Go语言流式输出实战:构建高性能实时应用
  • 数据结构(力扣刷题)
  • 蜂窝通信模组OpenCPU的介绍
  • REST-assured获取响应数据详解
  • 手写链路追踪优化-自动全局追踪代替局部手动追踪
  • 做一个实用的节假日工具
  • Java面试-spring boot框架
  • 98、23种设计模式之代理模式(7/23)
  • 【SpringMVC】SSM框架【二】——SpringMVC超详细
  • ModuleNotFoundError: No module named ‘cairosvg‘
  • 浔川社团阅读量破历史记录
  • 得物25年春招-安卓部分编程题
  • GD32入门到实战21--输入捕获
  • 【C++】日期类实现详解:代码解析与复用优化
  • C#正则表达式与用法
  • 【基础-单选】关于Tabs组件页签的位置设置,下面描述错误的是
  • 免费在线图片合成视频工具 ,完全免费
  • uni.onBLECharacteristicValueChange接收到数据,返回的value为{}的原因及其获取方法
  • 佳易王钟表维修养护管理系统:开启钟表维修高效管理新篇章​就#软件操作教程
  • Mysql 学习day 2 深入理解Mysql索引底层数据结构
  • React前端开发_Day6-Day9_极客园项目
  • C语言 - 输出参数详解:从简单示例到 alloc_chrdev_region
  • Spring AI 的应用和开发
  • 如何简单建设一个网站,让用户快速找到你。
  • 在PowerPoint和WPS演示让蝴蝶一直跳8字舞
  • Python生成免安装exe
  • SAP PP模块的MPS
  • Vue加载速度优化,verder.js和element.js加载速度慢解决方法
  • 防火墙技术(二):安全区域