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

【JavaEE初阶】线程 和 thread

本节⽬标
  • 认识多线程
  • 掌握多线程程序的编写
  • 掌握多线程的状态

一. 认识线程(Thread)

1概念

1) 线程是什么

       

         ⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.

        还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业务。我们进⼀步设想如下场景:

        ⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。
        如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。
        此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队 执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。

2) 为啥要有线程

⾸先, "并发编程" 成为 "刚需".

  • 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU资源.
  • 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.(线程就是轻量级进程)

  • 创建线程⽐创建进程更快.
  • 销毁线程⽐销毁进程更快.
  • 调度线程⽐调度进程更快.

最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池"(ThreadPool) 和 "协程"(Coroutine)

3) 进程和线程的区别

  • 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
⽐如之前的多进程例⼦中,每个客⼾来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则钱不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。
  • 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
  • ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).

4) Java 的线程 和 操作系统线程 的关系

        线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供用户使⽤(例如 Linux 的 pthread 库).
api:application programming interface(应用程序编程接口)
  1. 操作系统提供的原生api是c写的
  2. 不同操作系统的线程api不相同
Java 标准库中 Thread 类 可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装.(Thread类就在Java默认导入的java.lang包里面)

2 第⼀个多线程程序

感受多线程程序和普通程序的区别:
  • 每个线程都是⼀个独⽴的执⾏流
  • 多个线程之间是 "并发" 执⾏的.
  使⽤ jconsole 命令观察线程

3 创建线程

⽅法1 继承 Thread 类

(1)继承 Thread 来创建⼀个线程类.
class MyThread extends Thread{
    @Override
    //run相当于线程的入口函数
    public void run() {
        System.out.println("hello world");
    }
}

(2)创建 MyThread 类的实例

Thread t=new MyThread();

(3)调⽤ start ⽅法启动线程

 //真正在系统中创建出一个线程
 t.start();

(4)休眠

 try {
       Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
      } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

(5)run

        在执行线程时,不需要显示运行run方法,JVM自动调用

Tip:

在自己写的MyThread类里面不允许用throws,只能try-catch,因为其父类Thread里面没有实现该功能,但main函数中可以

实际开发中,异常的处理方式
1.记录异常信息作为日志.后续程序员根据日志调查问题
        程序仍然正常往后执行逻辑,不会因为这个异常就终止(对于服务器非常关键的)

2.进行重试,有的异常是概率性的(网络通讯)

3.特别严重的问题,必须立即马上处理的问题

        通过短信/邮件/微信/电话 通知程序员 (报警机制)


服务器和客户端指的是两个程序

服务器(server):被动接受请求,返回响应的一方

客户端(client):主动发起请求的一方

  • 客户端给服务器发送的数据,叫做“request"请求
  • 服务器给客户端返回的叫做“response”响应
  • 通常一个服务器可以给多个客户端提供服务
  • 服务器基本7*24待命

根据输出可以知道:多个线程的调度是随机的(“抢占式执行”)


Q:可以控制输出顺序吗?

A:输出顺序是操作系统内核的调度器控制的,没法在应用应用程序中编写代码控制 (调度器没有提供 api 的)
唯一能做的就是给线程设置优先级(但是优先级,对于操作系统来说,也是仅供参考,不会严格的定量的遵守)


如果直接使用run方法,而没有start,那么MyTread实质上没有创建出进程,只有main进程,遇到run中的死循环之后无法退出。

package Thread;
class MyThread extends Thread{
    @Override
    //run相当于线程的入口函数
    public void run() {
        while(true){
            System.out.println("hello run");
            try {
                Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new MyThread();
        //真正在系统中创建出一个线程
        //t.start();
        t.run();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

⽅法2 实现 Runnable 接⼝

1. 实现 Runnable 接⼝
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        }

}

2. 创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.

Runnable runnable=new MyRunnable();
Thread t=new Thread(runnable);

3. 调⽤ start ⽅法

 t.start();

总代码:

package Thread;
class MyRunnable implements Runnable{

    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        }

}

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=new MyRunnable();
        Thread t=new Thread(runnable);
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

Runnable最终还是要通过 Thread,真正创建线程

线程里要干啥, 通过 Runnable 来表示(而不是通过直接重写 Thread的run 来表示了)

Q:如何判断是用哪种?

A:根据线程要执行的任务的定义,是放到 Thread 里面,还是放到外面(Runnable 中)

Q:使用Runnable有什么好处吗?

A:解耦合。要执行的任务本身,和 线程这个概念,能够解耦合,从而后续如果变更代码.(比如不通过线程执行这个任务,通过其他方式.….)

采用 Runnable 这样的方案,代码的修改就会更简单.

对⽐上⾯两种⽅法:

  • 继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
  • 实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤Thread.currentThread()

⽅法3 实现Tread的匿名内部类

package Thread;

public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(){
              while(true){
                System.out.println("hello run");
                try {
                    Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        thread.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

⽅法4 匿名内部类创建 Runnable ⼦类对象

 
package Thread;

public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello run");
                    try {
                        Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread thread=new Thread(runnable);
        thread.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

⽅法5  lambda 表达式创建 Runnable ⼦类对象

package Thread;

public class demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while (true) {
                System.out.println("hello run");
                try {
                    Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

4 多线程的优势-增加运⾏速度

可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
  • 使⽤ System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
  • serial 串⾏的完成⼀系列运算.
  • concurrency 使⽤两个线程并⾏的完成同样的运算.
 

二. Thread 类及常⻅⽅法

      

        Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。

        ⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。

1 Thread 的常⻅构造⽅法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

main方法结束了,主线程就结束了

以前认知里main方法结束,程序就执行完毕是针对单线程程序的

2 Thread 的⼏个常⻅属性

  • ID 是线程的唯⼀标识,不同线程不会重复
  • 名称是各种调试⼯具⽤到
  • 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
  • 优先级⾼的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
  • 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
  • 线程的中断问题,下⾯我们进⼀步说明
 
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还在");
                    Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {
        }
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

3 启动⼀个线程 - start()

之前我们已经看到了如何通过覆写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。

  • 覆写 run ⽅法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • ⽽调⽤ start() ⽅法,就是喊⼀声:”⾏动起来!“,线程才真正独⽴去执⾏了。

调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程

4 中断⼀个线程

        李四⼀旦进到⼯作状态,他就会按照⾏动指南上的步骤去进⾏⼯作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如⽼板突然来电话了,说转账的对⽅是个骗⼦,需要赶紧停⽌转账,那张三该如何通知李四停⽌呢?这就涉及到我们的停⽌线程的⽅式了。

⽬前常⻅的有以下两种⽅式:

  • 通过共享的标记来进⾏沟通
  • 调⽤ interrupt() ⽅法来通知

⽰例1: 使⽤⾃定义的变量来作为标志位.

//需要给标志位上加 volatile 关键字(这个关键字的功能后⾯介绍).
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了⼤事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
        target.isQuit = true;
    }
}

⽰例-2: 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.

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

使⽤ thread 对象的 interrupted() ⽅法通知线程结束.
 
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
        // 两种⽅法均可以
            while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName() + ": 有内⻤,终⽌交易!");
                    // 注意此处的 break
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 啊!险些误了⼤事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName() + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName() + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
        thread.interrupt();
    }
}

thread 收到通知的⽅式有两种:

1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志

        ◦ 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.

          ◦ Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到。

      

5 等待⼀个线程 - join()

有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个⽅法明确等待线程的结束。

package Thread;

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target =() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            + ": 我还在⼯作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };

        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始⼯作");
        thread1.start();
        thread1.join();
        System.out.println("李四⼯作结束了,让王五开始⼯作");
        thread2.start();
        thread2.join();
        System.out.println("王五⼯作结束了");
    }
}

⼤家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?

附录

6 获取当前线程引⽤

这个⽅法我们已经⾮常熟悉了

public class ThreadDemo {
    public static void main(String[] args) {
            Thread thread = Thread.currentThread();
             System.out.println(thread.getName());
          }
}

7 休眠当前线程

也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。
 
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

三、 线程的状态

1 观察线程的所有状态

线程的状态是⼀个枚举类型
 
Thread.State
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

  • NEW: 安排了⼯作, 还未开始⾏动
  • RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.
  • BLOCKED: 这⼏个都表⽰排队等着其他事情
  • WAITING: 这⼏个都表⽰排队等着其他事情
  • TIMED_WAITING: 这⼏个都表⽰排队等着其他事情
  • TERMINATED: ⼯作完成了.

2. 线程状态和状态转移的意义

⼤家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。

还是我们之前的例⼦:

刚把李四、王五找来,还是给他们在安排任务,没让他们⾏动起来,就是 NEW 状态;

当李四、王五开始去窗⼝排队,等待服务,就进⼊到 RUNNABLE 状态。该状态并不表⽰已经被银⾏⼯作⼈员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;

当李四、王五因为⼀些事情需要去忙,例如需要填写信息、回家取证件、发呆⼀会等等时,进⼊BLOCKED 、 WATING 、TIMED_WAITING 状态,⾄于这些状态的细分,我们以后再详解;

如果李四、王五已经忙完,为 TERMINATED 状态。

所以,之前我们学过的 isAlive() ⽅法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的

3 观察线程的状态和转移

观察 1: 关注 NEW RUNNABLE TERMINATED 状态的转换  
package Thread;

public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000_0000; i++) {
            }
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());
        ;
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());
            ;
        }
        System.out.println(t.getName() + ": " + t.getState());
        ;
    }
}


观察 2: 关注 WAITING BLOCKED TIMED_WAITING 状态的转换
 public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

使⽤ jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

修改上⾯的代码, 把 t1 中的 sleep 换成 wait
 
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    try {
            // [修改这⾥就可以了!!!!!]
            // Thread.sleep(1000);
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1");
...
    }

使⽤ jconsole 可以看到 t1 的状态是 WAITING

结论:

BLOCKED 表⽰等待获取锁, WAITING 和 TIMED_WAITING 表⽰等待其他线程发来通知.

TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在⽆限等待唤醒


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

相关文章:

  • 技术文档的规划布局:打造清晰且有条理的知识传递框架
  • 基于Springboot人口老龄化社区服务与管理平台【附源码】
  • 胡九道:经典传承(贵宾酒)
  • javax.net.ssl.SSLPeerUnverifiedException: Hostname 192.168.13.13 not verified:
  • STM32MP1linux根文件系统目录作用
  • 揭秘区块链隐私黑科技:零知识证明如何改变未来
  • Mysql迁移达梦大批量数据报错处理_踩坑总结
  • 【Git从入门到精通】——新版IDea集成Git、Idea集成Github、Gitee以及GItLab应用(看这一篇就够了)
  • 鸿蒙审核版本页面显示异常之混淆代码问题
  • MFC 文档模板 每个文档模板需要实例化吧
  • Note20241220_一种组态王Modbus模拟通讯仿真实现方案
  • 《探秘 QT 5.14.1 类库的奇妙世界》
  • html 中 表格和表单的关系与区别
  • 连通“数据”,让制造变“聪明”
  • Leetcode经典题15-- 找出字符串中第一个匹配项的下标(KMP)
  • JS CSS HTML 的代码如何快速封装
  • 使用 Lambda 创建 Authorizer 对 API Gateway 访问进行鉴权
  • Mybatis分页插件的使用问题记录
  • 后摩尔定律时代,什么将推动计算机性能优化的发展?
  • Halcon 机器视觉案例 之 药剂液面高度测量
  • flutter 快速实现侧边栏
  • 软件架构设计方法之The Clean Architecture 整洁架构
  • android opencv导入进行编译
  • 使LED每秒闪烁一次
  • 海外招聘丨埃因霍温科技大学—安全人工智能自动机器学习博士后
  • 系统设计:微服务架构的可扩展性系统 详解