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

DAY13 线程池、死锁、线程状态、计时器

学习目标

能够描述Java中线程池运行原理
	使用一个容器(集合)存储一些线程
	当我们要使用线程的时候,从容器中取出
	使用完毕线程,把线程在归还给线程池
	可以提高程序的效率,避免了频繁的创建线程和销毁线程
能够描述死锁产生的原因
	出了两个同步代码块的嵌套
	线程一拿着线程二的锁
	线程二拿着线程一的锁
	两个线程都处于阻塞状态,都不会继续执行
能够说出线程6个状态的名称
	新建,运行,阻塞,睡眠(计算等待),无限等待,死亡
能够理解等待唤醒案例
	两个线程之间的协作
	包子铺线程做好包子-->唤醒吃货线程-->包子铺线程等待-->吃货线程吃包子-->吃完包子-->唤醒包子铺线程-->吃货线程等待
能够使用定时器
	void schedule(TimerTask task, long delay) 在指定的毫秒值之后,只执行一次定时任务
	void schedule(TimerTask task, long delay, long period) 在指定的毫秒值之后,每隔多少毫秒反复执行定时任务
	void schedule(TimerTask task, Date time) 在指定的时间和日期之后,只执行一次定时任务
	void schedule(TimerTask task, Date firstTime, long period) 在指定的时间和日期之后,每隔多少毫秒反复执行定时任务

请添加图片描述

第一章 线程池

1.线程池的思想

请添加图片描述

2.线程池概述

/*
    java.util.concurrent.Executors:是一个创建线程池的工具类,专门用来生产线程池,里边的方法都是静态的
    静态方法:
        static ExecutorService newFixedThreadPool(int nThreads)
            创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
        参数:
            int nThreads:创建的线程池,包含的线程数量 100 包含100个线程 1000包含1000个线程
        返回值:
            ExecutorService:就是一个线程池,ExecutorService是一个接口,返回的是ExecutorService接口的实现类对象
    java.util.concurrent.ExecutorService:描述线程池的接口
        Future<?> submit(Runnable task) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
        <T> Future<T> submit(Callable<T> task) 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
        参数:
           Runnable task:传递Runnable接口的实现类对象(线程任务)==>重写run方法,设置线程任务==>run方法没有返回值
           Callable<T> task:传递Callable接口的实现类对象(线程任务)==>重写call方法,设置线程任务==>call方法有返回值
        返回值:
            Future:用来接收线程任务的返回值==>用来接收call方法的返回值
 */

3.使用线程池执行Runnable接口的线程任务(重点)

package com.itheima.demo01ThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    使用线程池执行Runnable接口的线程任务(重点)
        1.使用Executors线程池工厂类的静态方法newFixedThreadPool创建一个包含指定线程数量的线程池ExecutorService
        2.创建Runnable接口|Callable接口的实现类对象,重写run方法|call方法,设置线程任务
        3.使用线程池ExecutorService中的方法submit,传递线程任务(Runnable接口|Callable接口的实现类对象),执行线程任务
          submit方法会自动在线程池中获取一个线程,执行线程任务,使用完毕会自动把线程在归还给线程池
 */
public class Demo01ThreadPool {
    public static void main(String[] args) {
        //1.使用Executors线程池工厂类的静态方法newFixedThreadPool创建一个包含指定线程数量的线程池ExecutorService
        ExecutorService es = Executors.newFixedThreadPool(3);//创建包含3个线程的线程池

        //3.使用线程池ExecutorService中的方法submit,传递线程任务(Runnable接口|Callable接口的实现类对象),执行线程任务
        //new Thread(new RunnableImpl()).start();
        es.submit(new RunnableImpl());//pool-1-thread-1线程正在执行线程任务!
        es.submit(new RunnableImpl());//pool-1-thread-2线程正在执行线程任务!
        es.submit(new RunnableImpl());//pool-1-thread-1线程正在执行线程任务!
        es.submit(new RunnableImpl());//pool-1-thread-3线程正在执行线程任务!

        //匿名内部类
        es.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"线程正在执行线程任务!");
            }
        });

        /*
            线程池中的方法(了解)
                 void shutdown()  用于销毁线程池
            注意:
                线程池一旦销毁,就不能在使用了,会抛出异常
         */
        es.shutdown();

        es.submit(new RunnableImpl());//RejectedExecutionException
    }
}
package com.itheima.demo01ThreadPool;

//2.创建Runnable接口|Callable接口的实现类对象,重写run方法|call方法,设置线程任务
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程正在执行线程任务!");
    }
}

4.使用线程池执行Callable接口的线程任务(重点)

package com.itheima.demo01ThreadPool;

import java.util.Random;
import java.util.concurrent.*;

/*
    使用线程池执行Callable接口的线程任务(重点)
    java.util.concurrent.Callable<V>接口
        返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。
        Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。
            但是 Runnable 不会返回结果,但是Callable有返回的结果
    Callable接口中的方法:
        V call() 计算结果,如果无法计算结果,则抛出一个异常。
        返回值:
            V:返回的是一个指定泛型的值,接口使用什么泛型,就返回一个什么类型的值
                接口的泛型使用String,返回一个String类型的值
                接口的泛型使用Integer,返回一个Integer类型的值
    Callable的作用:
        重写call方法,设置线程任务把线程任务交给线程池执行,执行的这个任务多了一个返回值
 */
public class Demo02ThreadPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.使用Executors线程池工厂类的静态方法newFixedThreadPool创建一个包含指定线程数量的线程池ExecutorService
        ExecutorService es = Executors.newFixedThreadPool(3);

        //2.创建Runnable接口|Callable接口的实现类对象,重写run方法|call方法,设置线程任务
        Callable<Integer> c =  new Callable<Integer>(){
            @Override
            public Integer call() throws Exception {
                //返回一个随机整数
                return new Random().nextInt(10);//返回[0-9]之间的一个随机整数
            }
        };

        //3.使用线程池ExecutorService中的方法submit,传递线程任务(Runnable接口|Callable接口的实现类对象),执行线程任务
        Future<Integer> future = es.submit(c);
        System.out.println(future);//java.util.concurrent.FutureTask@14ae5a5

        /*
            java.util.concurrent.Future<V>接口
                Future 表示异步计算的结果。用来接收call方法的返回值
            Future接口中的方法:
                V get() 如有必要,等待计算完成,然后获取其结果。
         */
        Integer v = future.get();
        System.out.println(v);

        Future<Double> f2 = es.submit(new Callable<Double>() {
            @Override
            public Double call() throws Exception {
                //返回一个随机小数[0.0-1.0)
                return Math.random();
            }
        });
        System.out.println(f2.get());
    }
}

5.线程池的练习

package com.itheima.demo01ThreadPool;

import java.util.Scanner;
import java.util.concurrent.*;

/*
    线程池的练习
        需求: 使用线程池方式执行任务,返回1-n的和
        分析: 因为需要返回求和结果,所以使用Callable方式的任务
    实现步骤:
        1.使用Scanner获取一个用户输入的整数
        2.使用Executors线程池工厂类的静态方法newFixedThreadPool创建一个包含指定线程数量的线程池ExecutorService
        3.创建Callable接口的实现类对象,重写call方法,设置线程任务
        4.使用线程池ExecutorService中的方法submit,传递线程任务(Callable接口的实现类对象),执行线程任务
 */
public class Demo03Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.使用Scanner获取一个用户输入的整数
        System.out.println("请输入一个整数,计算1-n的和:");
        int n = new Scanner(System.in).nextInt();
        //2.使用Executors线程池工厂类的静态方法newFixedThreadPool创建一个包含指定线程数量的线程池ExecutorService
        ExecutorService es = Executors.newFixedThreadPool(3);
        //3.创建Callable接口的实现类对象,重写call方法,设置线程任务
        //4.使用线程池ExecutorService中的方法submit,传递线程任务(Callable接口的实现类对象),执行线程任务
        Future<Integer> f = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                //线程任务:计算1-n和和并返回
                //定义一个变量,初始值为0,记录累加求和
                int sum = 0;
                //使用for循环获取1-n之间的数字
                for (int i = 1; i <= n; i++) {
                    //累加求和
                    sum+=i;
                }
                //把和返回
                return sum;
            }
        });
        System.out.println("使用线程池计算1-"+n+"之间的和为:"+f.get());
    }
}

第二章 死锁(面试)

1.死锁的原理

请添加图片描述

2.死锁的代码实现(重点)

package com.itheima.demo02DeadLock;

/*
    死锁:线程获取不到锁对象,从而进不去同步中执行
    前提:
        1.必须出现同步代码块的嵌套
        2.必须有两个线程
        3.必须有两个锁对象
    同步中的线程没有出同步是不会释放锁对象的,没有锁的对象进不去同步
 */
public class RunnableImpl implements Runnable{
    //创建两个锁对象,可以是任意的对象,保证唯一
    private String lockA = "A锁";
    private String lockB = "B锁";
    //定义一个变量,用于设置两个线程交叉执行,一人执行一次
    int a = 0;
    @Override
    public void run() {
        //定义一个死循环,让线程任务重复执行
        while (true){
            //判断变量a是奇数还是偶数
            if(a%2==0){//是偶数,让一个线程执行
                //同步代码块嵌套
                synchronized (lockA){
                    System.out.println(Thread.currentThread().getName()+"...into if lockA...");
                    synchronized (lockB){
                        System.out.println(Thread.currentThread().getName()+"...into if lockB...");
                    }
                }
            }else{//奇数,让另外一个线程执行
                //同步代码块嵌套
                synchronized (lockB){
                    System.out.println(Thread.currentThread().getName()+"...into else lockB...");
                    synchronized (lockA){
                        System.out.println(Thread.currentThread().getName()+"...into else lockA...");
                    }
                }
            }
            a++;//每循环一次,都改变变量的值
        }
    }
}
package com.itheima.demo02DeadLock;

public class Demo01DealLook {
    public static void main(String[] args) {
        //创建两个线程,同时执行线程任务
        RunnableImpl r = new RunnableImpl();
        new Thread(r).start();
        new Thread(r).start();
    }
}

执行结果

Thread-0...into if lockA...
Thread-0...into if lockB...
Thread-0...into else lockB...
Thread-0...into else lockA...
Thread-0...into if lockA...
Thread-0...into if lockB...
Thread-0...into else lockB...
Thread-0...into else lockA...
Thread-0...into if lockA...
Thread-1...into else lockB...
    
    
Thread-0...into if lockA...
Thread-0...into if lockB...
Thread-0...into else lockB...
Thread-1...into if lockA...    

注意:我们平时写代码应该尽量避免死锁

第三章 线程状态

1.线程状态概述

重点:必须记住6种线程状态的名称
请添加图片描述

2.Object类中等待与唤醒的方法

java.lang.Object:里边的方法,任意的一个类都可以使用
void wait() 
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 
void notify() 
    醒在此对象监视器(同步锁,对象锁)上等待的单个线程。 
void notifyAll() 
    唤醒在此对象监视器(同步锁,对象锁)上等待的所有线程。 
注意:
	1.wait方法和notify方法一般都是使用在同步代码块中==>有锁对象==>对象监视器
	2.一般都是使用锁对象调用wait和notify方法(多个线程使用的是同一个锁对象)
        锁对象(Thread-0)-->wait方法-->Thread-0线程-->等待
        锁对象(Thread-1)-->notify方法-->唤醒在锁对象上等待的线程-->唤醒Thread-0线程
    3.在同步中的线程调用wait方法,进入到等待,会释放锁对象的
      在同步中线程调用sleep方法,进入睡眠,不会释放锁对象的

3.等待与唤醒案例(重点)

a.需求分析

请添加图片描述

b.代码实现

package com.itheima.demo03WaitAndNotify;

/*
    1.包子类:资源类
	属性:皮,馅,状态(有|没有)
 */
public class BaoZi {
    //皮
    String pi;
    //馅
    String xian;
    //包子状态:初始值为false(没有包子)
    boolean flag = false;
}
package com.itheima.demo03WaitAndNotify;

/*
    2.包子铺类:是一个线程类(继承Thread,实现Runnable接口)
    线程任务:做包子
    对包子的状态进行判断
      true:有包子
        包子铺线程等待 包子对象.wait();
      false:没有包子
        包子铺线程做包子
        打印做x皮x馅的包子
        花3秒钟做包子
        3秒钟之后做好包子
        修改包子的状态有
        唤醒吃货线程吃包子
    注意:
        必须保证包子铺线程和吃货线程只能有一个在运行(要么做包子,要么吃包子)
        可以使用同步代码块==>需要一个锁对象,必须保证两个线程使用的是同一个锁对象
        可以使用包子对象作为锁对象(唯一)
        定义一个包子类型的变量,使用构造方法为变量赋值
 */
//包子铺类:是一个线程类(继承Thread,实现Runnable接口)
public class BaoZiPu implements Runnable{
    //定义一个包子类型的变量,使用构造方法为变量赋值
    private BaoZi bz;

    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }

    //线程任务:做包子
    @Override
    public void run() {
        //增加一个死循环,让包子铺线程一直做包子
        while (true){
            //必须保证包子铺线程和吃货线程只能有一个在运行(要么做包子,要么吃包子)==>同步代码块
            synchronized (bz){
                //对包子的状态进行判断
                if(bz.flag==true){
                    //true:有包子 包子铺线程等待 包子对象.wait();
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //false:没有包子 包子铺线程做包子 包子铺线程被吃货线程唤醒之后执行的代码
                //打印做x皮x馅的包子
                bz.pi = "薄皮";
                bz.xian = "牛肉大葱馅";
                System.out.println("包子铺线程正在做:"+bz.pi+bz.xian+"的包子!");

                // 花3秒钟做包子
                System.out.println("包子铺做包子需要3秒钟时间!");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //3秒钟之后做好包子,修改包子的状态有
                bz.flag = true;
                //唤醒吃货线程吃包子
                bz.notify();//唤醒包子对象上等待的吃货线程
                System.out.println("包子铺线程已经做好了"+bz.pi+bz.xian+"的包子,吃货线程赶紧来吃包子吧!");
            }
        }
    }
}
package com.itheima.demo03WaitAndNotify;

/*
    3.吃货类:是一个线程类
    线程任务:吃包子
    对包子状态进行判断
      false:没有包子
        吃货线程等待 包子对象.wait();
      true:有包子
        吃货线程吃包子
        打印吃x皮x馅的包子
        花1秒钟吃包子
        1秒钟之后吃完包子
        修改包子的状态为:没有
        唤醒包子铺线程做包子
    注意:
        必须保证包子铺线程和吃货线程只能有一个在运行(要么做包子,要么吃包子)
        可以使用同步代码块==>需要一个锁对象,必须保证两个线程使用的是同一个锁对象
        可以使用包子对象作为锁对象(唯一)
        定义一个包子类型的变量,使用构造方法为变量赋值
 */
//吃货类:是一个线程类
public class ChiHuo implements Runnable{
    //定义一个包子类型的变量,使用构造方法为变量赋值
    private BaoZi bz;

    public ChiHuo(BaoZi bz) {
        this.bz = bz;
    }

    //线程任务:吃包子
    @Override
    public void run() {
        //增加一个死循环,让吃货线程一直吃包子
        while (true){
            //必须保证包子铺线程和吃货线程只能有一个在运行(要么做包子,要么吃包子)==>同步代码块
            synchronized (bz){
                //对包子状态进行判断
                if(bz.flag==false){
                    //false:没有包子 吃货线程等待 包子对象.wait();
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //true:有包子 吃货线程吃包子
                //打印吃x皮x馅的包子
                System.out.println("吃货正在吃"+bz.pi+bz.xian+"的包子!");
                //花1秒钟吃包子
                System.out.println("吃货吃包子需要花1秒钟!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //1秒钟之后吃完包子,修改包子的状态为:没有
                bz.flag = false;
                //唤醒包子铺线程做包子
                bz.notify();//唤醒是包子对象上等待的包子铺线程
                System.out.println("吃货线程已经吃完"+bz.pi+bz.xian+"的包子了,包子铺线程赶紧做包子吧!");
                System.out.println("-----------------------------------------------------------------------");
            }
        }
    }
}
package com.itheima.demo03WaitAndNotify;

/*
    4.测试类(包含main方法的类)
    创建包子对象,创建2个线程,一个包子铺线程做包子,一个吃货线程吃包子
 */
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        //创建包子对象
        BaoZi bz = new BaoZi();
        //创建2个线程,一个包子铺线程做包子,一个吃货线程吃包子
        new Thread(new BaoZiPu(bz)).start();
        new Thread(new ChiHuo(bz)).start();
    }
}

请添加图片描述

第四章 定时器

1.定时器的概述

定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情

/*
    java.util.Timer类:描述定时器的类
        一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
    构造方法:
        Timer() 创建一个新计时器。
    成员方法:
        void cancel() 终止此计时器,丢弃所有当前已安排的任务。
            注意,在此计时器调用的计时器任务的 run 方法内调用此方法,就可以绝对确保正在执行的任务是此计时器所执行的最后一个任务。
        void schedule(TimerTask task, long delay) 在指定毫秒值之后,执行指定的任务,只会执行一次
            参数:
                task - 所要安排的任务。定时器执行的任务
                delay - 执行任务前的延迟时间,单位是毫秒。多少毫秒之后开始执行TimerTask任务
        void schedule(TimerTask task, long delay, long period) 在指定毫秒值之后,执行指定的任务,之后每隔固定的毫秒数重复的执行定时任务
            参数:
                task - 所要安排的任务。定时器执行的任务
                delay - 执行任务前的延迟时间,单位是毫秒。多少毫秒之后开始执行TimerTask任务
                period - 执行各后续任务之间的时间间隔,单位是毫秒。定时器开始执行之后,每隔多少毫秒重复执行
        void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务,只会执行一次
            参数:
                task - 所要安排的任务。定时器执行的任务
                time - 执行任务的时间。从什么日期(Date对象)开始执行任务 2020-05-12 14:45:30
        void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。
            参数:
                task - 所要安排的任务。定时器执行的任务
                firstTime - 首次执行任务的时间。从什么日期(Date对象)开始执行任务 2020-05-12 14:45:30
                period - 执行各后续任务之间的时间间隔,单位是毫秒。 定时器开始执行之后,每隔多少毫秒重复执行
    java.util.TimerTask:类 implements Runnable接口
        由 Timer 安排为一次执行或重复执行的任务。
        void run() 此计时器任务要执行的操作。重写run方法,设置线程任务 
 */

2.定时器的使用(重点)

public class Demo01Timer {
    public static void main(String[] args) throws ParseException {
        show04();
    }

    /*
         void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。
            参数:
                task - 所要安排的任务。定时器执行的任务
                firstTime - 首次执行任务的时间。从什么日期(Date对象)开始执行任务 2020-05-12 14:45:30
                period - 执行各后续任务之间的时间间隔,单位是毫秒。 定时器开始执行之后,每隔多少毫秒重复执行
     */
    private static void show04() throws ParseException {
        /*
            创建一个反复执行的定时器: 2020-05-12 15:06:15 开始第一次执行,之后每隔1秒钟执行1次
         */
        Timer timer = new Timer();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse("2020-05-12 15:05:50");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("嘿嘿!");
            }
        },date,1000);
    }

    /*
        void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务,只会执行一次
            参数:
                task - 所要安排的任务。定时器执行的任务
                time - 执行任务的时间。从什么日期(Date对象)开始执行任务 2020-05-12 15:02:15
     */
    private static void show03() throws ParseException {
        /*
            创建一个执行一次的定时器:2020-05-12 15:02:15
         */
        Timer timer = new Timer();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse("2020-05-12 15:02:15");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("哈哈!");
                timer.cancel();//执行完定时任务之后,取消定时器
            }
        },date);
    }

    /*
        void schedule(TimerTask task, long delay, long period) 在指定毫秒值之后,执行指定的任务,之后每隔固定的毫秒数重复的执行定时任务
            参数:
                task - 所要安排的任务。定时器执行的任务
                delay - 执行任务前的延迟时间,单位是毫秒。多少毫秒之后开始执行TimerTask任务
                period - 执行各后续任务之间的时间间隔,单位是毫秒。定时器开始执行之后,每隔多少毫秒重复执行
     */
    private static void show02() {
        /*
            创建一个反复执行的定时器,5秒钟之后开始第一次执行,之后每隔1秒钟执行一次
         */
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //打印系统时间
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                System.out.println(sdf.format(new Date()));
            }
        },5000,1000);
    }

    /*
        void schedule(TimerTask task, long delay) 在指定毫秒值之后,执行指定的任务,只会执行一次
            参数:
                task - 所要安排的任务。定时器执行的任务
                delay - 执行任务前的延迟时间,单位是毫秒。多少毫秒之后开始执行TimerTask任务
     */
    private static void show01() {
        /*
            创建一个执行一次的定时器,5秒钟之后开始执行
         */
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("c4爆炸了!");
                //执行完了定义任务,结束定时器
                timer.cancel();
            }
        },5000);
    }
}

http://www.kler.cn/a/595564.html

相关文章:

  • 如何在ubuntu上安装zookeeper
  • 在 Elasticsearch 中探索基于 NVIDIA 的 GPU 加速向量搜索
  • w265基于Spring Boot库存管理系统
  • 【C++】static、内部类
  • Vue.js 过滤器(Filters):简化文本格式化的利器
  • 测试Claude3.7 sonnet画蛋白质
  • 「AI+办公」共学(四):办公综合应用
  • 实现拖拽图片验证的基本步骤
  • AI 时代的通信新范式:MCP(模块化通信协议)的优势与应用
  • 关于神经网络中的激活函数
  • 第2.1节:AWK脚本结构
  • Resume全栈项目(.NET)
  • 华为总部-华为东莞松山湖基地参观游学攻略
  • 【DeepSeek 学C+】effective modern c+ 条款七 初始化
  • PurpleLlama大模型安全全套检测方案
  • OpenCV ML 模块使用指南
  • 机器学习-04-分类算法-02贝叶斯算法案例
  • 电脑自动关机故障维修案例分享
  • FRP在远程办公中的实战应用
  • 23种设计模式(扩展)