• JavaSE进阶、多线程


    目录

    一、线程的概述

     二、多线程的创建

    方式一、继承Thread类

    方式二、实现Runnable接口

    方式三、JDK5.0提供Callable、FutureTask接口

    三种方式的对比

    三、Thread的常用方法

    1、调用方法设置线程名称

    2、使用Thread构造器设置线程名称

    3、Thread类的线程休眠方法

    四、线程安全

    线程安全问题

    五、线程同步

     线程同步核心思想

    加锁方式一、同步代码块

    加锁方式二、同步方法

    加锁方式三、ReentrantLock重入锁

    六、线程通信

    七、线程池

    1、线程池的概述

    2、线程池实现的API

    线程池构造器参数

    线程池常见面试题

    3、线程池处理Runnable任务

    4、线程池处理Callable任务

    5、Executors工具类创建线程池

    八、定时器

    1、Timer定时器

     2、ScheduleExecutorService创建定时器

    九、补充知识:并发并行、生命周期


    一、线程的概述

     二、多线程的创建

    方式一、继承Thread类

    1. //使用多线程
    2. public class ThreadDemo1 {
    3. //默认执行的线程为主线程如main方法
    4. public static void main(String[] args) {
    5. //3、new一个新线程对象
    6. Thread t = new MyThread(); //多态
    7. //4、调用start方法,实际还是调用run方法
    8. t.start();
    9. for (int i = 0; i < 5; i++) {
    10. System.out.println("主线程" + i);
    11. }
    12. }
    13. }
    14. //1、定义一个线程类继承thread
    15. class MyThread extends Thread{
    16. //2、重写run方法,声明线程要做什么
    17. @Override
    18. public void run() {
    19. for (int i = 0; i < 5; i++) {
    20. System.out.println("子线程" + i);
    21. }
    22. }
    23. }
    24. //主线程与子线程同时跑
    25. 主线程0
    26. 子线程0
    27. 主线程1
    28. 子线程1
    29. 主线程2
    30. 子线程2
    31. 主线程3
    32. 子线程3
    33. 主线程4
    34. 子线程4
    1. public class Test {
    2. public static void main(String[] args) {
    3. Student s = new Student();
    4. s.run();
    5. for (int i = 0; i < 5; i++) {
    6. System.out.println("主测试" + i);
    7. }
    8. }
    9. }
    10. class Student{
    11. public void run(){
    12. for (int i = 0; i < 5; i++) {
    13. System.out.println("测试" + i);
    14. }
    15. }
    16. }
    17. //不使用多线程 按顺序进行
    18. 测试0
    19. 测试1
    20. 测试2
    21. 测试3
    22. 测试4
    23. 主测试0
    24. 主测试1
    25. 主测试2
    26. 主测试3
    27. 主测试4

    继承了一个类就不能继承其他类,不利于扩展。

    为什么用start调用run方法,而不直接用run方法?

    如果直接调用run方法,则会当作普通方法来执行,按顺序执行,而不会以多线程形式进行,start方法是告诉操作系统,调用出开启新的多线程,让CPU将他作为一个单独的执行流程,以线程的方式启动,要将主线程任务放在子线程之后

    方式二、实现Runnable接口

    1. public class ThreadDemo2 {
    2. public static void main(String[] args) {
    3. //学会线程创建方式二
    4. //2、创建任务对象,非线程对象
    5. Runnable r = new MyRunnable();
    6. //3、将任务对象交给Thread处理,转成线程对象
    7. Thread t = new Thread(r);
    8. //4、调用start方法,启动线程
    9. t.start();
    10. //主线程
    11. for (int i = 0; i < 3; i++) {
    12. System.out.println("主线程输出" + i);
    13. }
    14. }
    15. }
    16. class MyRunnable implements Runnable{
    17. //1、创建任务类继承Runable接口
    18. @Override
    19. public void run() {
    20. for (int i = 0; i < 3; i++) {
    21. System.out.println("子线程输出" + i);
    22. }
    23. }
    24. }
    1. public class ThreadDemo3 {
    2. public static void main(String[] args) {
    3. //学会线程创建方式二、匿名内部类
    4. //1、创建匿名任务对象
    5. Runnable r = new Runnable() {
    6. @Override
    7. public void run() {
    8. for (int i = 0; i < 3; i++) {
    9. System.out.println("子线程1输出" + i);
    10. }
    11. }
    12. };
    13. //2、将任务对象交给Thread处理,转成线程对象
    14. Thread t = new Thread(r);
    15. //3、调用start方法,启动线程
    16. t.start();
    17. //简化写法
    18. new Thread(new Runnable() {
    19. @Override
    20. public void run() {
    21. for (int i = 0; i < 3; i++) {
    22. System.out.println("子线程2输出" + i);
    23. }
    24. }
    25. }).start();
    26. //再简化
    27. new Thread(() -> {
    28. for (int i = 0; i < 3; i++) {
    29. System.out.println("子线程3输出" + i);
    30. }
    31. }).start();
    32. //主线程
    33. for (int i = 0; i < 3; i++) {
    34. System.out.println("主线程1输出" + i);
    35. }
    36. }
    37. }

    方式三、JDK5.0提供Callable、FutureTask接口

    1. public class ThreadDemo4 {
    2. public static void main(String[] args) {
    3. //2、创建Callable任务对象
    4. Callable call = new MyCallable(100);
    5. //3、将Callable任务对象 交给 FutureTask对象
    6. // FutureTask对象的作用1:FutureTask是Runnable的对象(实现了Runnable接口),可以交给Thread
    7. // FutureTask对象的作用2:可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
    8. FutureTask f = new FutureTask<>(call); //将call转成了Runnable的对象
    9. //4、交给线程处理
    10. Thread t = new Thread(f);
    11. //5、启动线程
    12. t.start();
    13. //6、获取线程执行的结果
    14. try {
    15. //如果直接用callable对象调用call方法时,call方法可能没执行完就输出了
    16. //使用get方法一旦发现线程没有执行完他就会让出cpu 暂停执行 直到线程执行完才继续执行
    17. String s = f.get();
    18. System.out.println("线程1的结果:" + s);
    19. } catch (Exception e) {
    20. e.printStackTrace();
    21. }
    22. //线程2
    23. Callable call2 = new MyCallable(200);
    24. FutureTask f2 = new FutureTask<>(call2);
    25. Thread t2 = new Thread(f2);
    26. t2.start();
    27. try {
    28. String s2 = f2.get();
    29. System.out.println("线程2的结果:" + s2);
    30. } catch (Exception e) {
    31. e.printStackTrace();
    32. }
    33. }
    34. }
    35. //1、创建任务类继承Callable接口 重写call方法
    36. class MyCallable implements Callable{
    37. private int n;
    38. public MyCallable() {
    39. }
    40. public MyCallable(int n) {
    41. this.n = n;
    42. }
    43. @Override
    44. public String call() throws Exception {
    45. int sum = 0;
    46. for (int i = 0; i < n; i++) {
    47. sum += n;
    48. }
    49. return "子线程执行的结果:" + sum;
    50. }
    51. }

    三种方式的对比

    三、Thread的常用方法

    1、调用方法设置线程名称

    1. public class ThreadAPIDemo {
    2. public static void main(String[] args) {
    3. Thread t = new MyThread();
    4. //设置线程的名字
    5. t.setName("1号");
    6. t.start();
    7. Thread t2 = new MyThread();
    8. t2.setName("2号");
    9. t2.start();
    10. //获取当前执行的线程对象
    11. //哪个线程执行它,它就得到哪个线程对象
    12. //主线程的名称就叫main
    13. Thread m = Thread.currentThread();
    14. for (int i = 0; i < 5; i++) {
    15. System.out.println("main线程输出" + i);
    16. }
    17. }
    18. }

    2、使用Thread构造器设置线程名称

    1. public class ThreadAPIDemo2 {
    2. public static void main(String[] args) {
    3. //直接通过父类构造器创建线程名
    4. Thread t1 = new Thread("1号");
    5. t1.start();
    6. }
    7. }
    8. //MyThread继承Thread继承父类构造器
    9. public class MyThread extends Thread{
    10. public MyThread() {
    11. }
    12. //继承父类构造器
    13. public MyThread(String name) {
    14. super(name);
    15. }
    16. @Override
    17. public void run() {
    18. for (int i = 0; i < 5; i++) {
    19. System.out.println(Thread.currentThread().getName() + "线程输出:" + i);
    20. }
    21. }
    22. }

    3、Thread类的线程休眠方法

    1. public static void main(String[] args) throws InterruptedException {
    2. for (int i = 1; i < 5; i++) {
    3. System.out.println("输出:" + i);
    4. if (i == 3){
    5. //如果i == 3 休眠3秒再执行
    6. Thread.sleep(3000);
    7. }
    8. }
    9. }

    四、线程安全

    线程安全问题

    两人同时取钱,会使得账户余额变为-10万导致银行亏了10万

    1. public class RemoveMoney {
    2. public static void main(String[] args) {
    3. //新建 账户
    4. Account a = new Account("共享账户",10);
    5. //两个线程接同一个账户
    6. //小红线程
    7. new User("小红",a).start();
    8. //小明线程
    9. new User("小明",a).start();
    10. }
    11. }
    12. //线程用户类
    13. public class User extends Thread{
    14. //关联账户类对象
    15. private Account acc;
    16. public User(String name, Account acc) {
    17. super(name);
    18. this.acc = acc;
    19. }
    20. public Account getAcc() {
    21. return acc;
    22. }
    23. public void setAcc(Account acc) {
    24. this.acc = acc;
    25. }
    26. @Override
    27. public void run() {
    28. acc.drawMonry(10);
    29. }
    30. }
    31. //账户类
    32. public class Account {
    33. private String accountName;
    34. private double money;
    35. public Account() {
    36. }
    37. public Account(String accountName, double money) {
    38. this.accountName = accountName;
    39. this.money = money;
    40. }
    41. ...
    42. /**
    43. * 用户取钱
    44. * @param i
    45. */
    46. public void drawMonry(double i) {
    47. //判断是哪一个线程执行
    48. String userName = Thread.currentThread().getName();
    49. //判断账户余额是否充足
    50. if (i <= this.money){
    51. System.out.println(userName + "来取钱成功,吐出:" + i);
    52. //更新余额
    53. this.money -= i;
    54. System.out.println(userName + "来取钱剩余:" + this.money);
    55. }else {
    56. System.out.println(userName + "取钱,余额不足!");
    57. }
    58. }
    59. }

    五、线程同步

    比如银行取钱,两个用户可以同时访问账户,但是到了取钱业务时开始排队,有先后次序。

     线程同步核心思想

    加锁方式一、同步代码块

    1. public class Account {
    2. private String accountName;
    3. private double money;
    4. .
    5. .
    6. .
    7. /**
    8. * 用户取钱
    9. * @param i
    10. */
    11. public void drawMonry(double i) {
    12. //判断是哪一个线程执行
    13. String userName = Thread.currentThread().getName();
    14. //取钱业务进行加锁,同步代码块
    15. //默认锁对象
    16. synchronized ("heima") {
    17. //判断账户余额是否充足
    18. if (i <= this.money){
    19. System.out.println(userName + "来取钱成功,吐出:" + i);
    20. //更新余额
    21. this.money -= i;
    22. System.out.println(userName + "来取钱剩余:" + this.money);
    23. }else {
    24. System.out.println(userName + "取钱,余额不足!");
    25. }
    26. }
    27. }
    28. }

     两个线程同步进行到此业务时,其中一个线程抢到锁则进入执行,同时将此业务暂时锁住,后来的线程只能等待先抢到锁的线程执行完毕,才能进入继续执行。

    问题引入:锁对象用任意唯一的对象好不好呢?(任意对象但类型唯一)

    因为这个锁是定义在账户类中,所有的账户类都共用同一个锁,当不同的线程对象使用不同的账户类时,他们所使用的锁是同一个,因此,线程A操作自己的账户A,线程B操作自己的账户B,当这两个线程同时运行,A操作A账户时,进行加锁,然而锁只有一个,所以B要操作自己的账户B 就必须等待A操作完,因此会影响其他无关线程的执行。

    1. //对每个账户对象都设了一把独有的锁
    2. public void drawMonry(double i) {
    3. //判断是哪一个线程执行
    4. String userName = Thread.currentThread().getName();
    5. //this 表示当前账户对象,对于不同账户对象会使用不同的锁
    6. synchronized (this) {
    7. //判断账户余额是否充足
    8. if (i <= this.money){
    9. System.out.println(userName + "来取钱成功,吐出:" + i);
    10. //更新余额
    11. this.money -= i;
    12. System.out.println(userName + "来取钱剩余:" + this.money);
    13. }else {
    14. System.out.println(userName + "取钱,余额不足!");
    15. }
    16. }
    17. }
    18. //对静态方法加锁
    19. //类名.class
    20. public static void run(){
    21. synchronized (Account.class){
    22. }
    23. }

    加锁方式二、同步方法

    1. //加修饰符对方法加锁
    2. public synchronized void drawMonry(double i) {
    3. //判断是哪一个线程执行
    4. String userName = Thread.currentThread().getName();
    5. //this 表示当前账户对象,对于不同账户对象会使用不同的锁
    6. //判断账户余额是否充足
    7. if (i <= this.money){
    8. System.out.println(userName + "来取钱成功,吐出:" + i);
    9. //更新余额
    10. this.money -= i;
    11. System.out.println(userName + "来取钱剩余:" + this.money);
    12. }else {
    13. System.out.println(userName + "取钱,余额不足!");
    14. }
    15. }

     

    但是同步代码块的可读性差,同步方法更容易看出

    加锁方式三、ReentrantLock重入锁

    1. public class Account {
    2. private String accountName;
    3. private double money;
    4. //对每个创建的账户类独有一个锁,并且锁唯一,不可被修改
    5. private final ReentrantLock lock = new ReentrantLock();
    6. .
    7. .
    8. .
    9. public void drawMonry(double i) {
    10. //判断是哪一个线程执行
    11. String userName = Thread.currentThread().getName();
    12. //上锁
    13. lock.lock();
    14. //判断账户余额是否充足
    15. try {
    16. if (i <= this.money) {
    17. System.out.println(userName + "来取钱成功,吐出:" + i);
    18. //更新余额
    19. this.money -= i;
    20. System.out.println(userName + "来取钱剩余:" + this.money);
    21. } else {
    22. System.out.println(userName + "取钱,余额不足!");
    23. }
    24. } catch (Exception e) {
    25. e.printStackTrace();
    26. } finally {
    27. //放在finally中使解锁一定执行
    28. //解锁
    29. lock.unlock();
    30. }
    31. }

    六、线程通信

    1. public static void main(String[] args) {
    2. //新建账户对象
    3. Account acc = new Account("CCC-233",0);
    4. //五个人用的同一把锁,锁可以跨方法
    5. //创建两个取钱线程代表小红、小明
    6. new UserThread("小红",acc).start();
    7. new UserThread("小明",acc).start();
    8. //创建三个存钱线程表示三个爸爸
    9. new DepositThread("岳父",acc).start();
    10. new DepositThread("干爹",acc).start();
    11. new DepositThread("亲爹",acc).start();
    12. }
    1. public class Account {
    2. private String accountName;
    3. private double money;
    4. //每个账户都有一个自己的锁,不能被更改
    5. private final ReentrantLock lock = new ReentrantLock();
    6. public Account() {
    7. }
    8. .
    9. .
    10. .
    11. /**
    12. * 取钱
    13. * @param money
    14. */
    15. public synchronized void drawMoney(double money) {
    16. //实例方法中this代表锁对象
    17. try {
    18. //取钱
    19. //获取到当前线程
    20. String name = Thread.currentThread().getName();
    21. //判断取钱是否大于账户
    22. if (this.money >= money) {
    23. this.money -= money;
    24. System.out.println(name + "取钱" + money + "成功!账户余额:" + this.money);
    25. //取完钱,没钱了
    26. //先将其他进程唤醒自己在进入等待
    27. //用当前进程唤醒所有进程
    28. this.notifyAll();
    29. //让当前进程进入等待
    30. this.wait();
    31. } else {
    32. //当前线程进入发现余额不足则唤醒其他线程,自己等待
    33. //用当前进程唤醒所有进程
    34. this.notifyAll();
    35. //让当前进程进入等待
    36. this.wait();
    37. }
    38. } catch (InterruptedException e) {
    39. e.printStackTrace();
    40. }
    41. }
    42. /**
    43. * 存钱
    44. * @param money
    45. */
    46. public synchronized void depositMoney(double money) {
    47. try {
    48. //存钱
    49. //获取到当前线程
    50. String name = Thread.currentThread().getName();
    51. //判断取钱是否大于账户
    52. if (this.money == 0) {
    53. this.money += money;
    54. System.out.println(name + "存钱成功!账户余额:" + this.money);
    55. //用当前进程唤醒所有进程
    56. this.notifyAll();
    57. //让当前进程进入等待
    58. this.wait();
    59. } else {
    60. //还有钱不用存
    61. //用当前进程唤醒所有进程
    62. this.notifyAll();
    63. //让当前进程进入等待
    64. this.wait();
    65. }
    66. } catch (InterruptedException e) {
    67. e.printStackTrace();
    68. }
    69. }
    70. }

    七、线程池

    1、线程池的概述

     

     假设线程池控制的最多只有三个长久存在的线程,在进入的新任务就不会创建新的线程,会等前三个线程执行完成后再来处理新线程,使线程可以重复利用,避免资源耗尽的风险,并且节约系统内存,因为如果进入一个任务就创建一个线程,线程是占用内存的,当任务过多会挤爆系统。

    2、线程池实现的API

    线程池构造器参数

     任务队列表示饭店门口的椅子,并不是实际工作的椅子,也就是允许等待的线程数量

    线程池常见面试题

    •  临时线程什么时候创建?

    新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

    • 什么时候会开始拒绝任务?

    核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

    3、线程池处理Runnable任务

     CallerRunsPolicy,由主线程负责处理,就是绕过员工交给老板处理

    1. //Runnable任务对象
    2. public class MyRunnable implements Runnable{
    3. @Override
    4. public void run() {
    5. for (int i = 0; i < 5; i++) {
    6. System.out.println(Thread.currentThread().getName() + "输出:Hello world" + "=>" + i);
    7. } try {
    8. System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,线程进入休眠");
    9. Thread.sleep(10000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    15. public class ThreadPoolDemo {
    16. public static void main(String[] args) {
    17. //1、创建线程池对象
    18. ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,
    19. new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
    20. new ThreadPoolExecutor.AbortPolicy());
    21. //2、创建任务对象给线程池处理
    22. Runnable target = new MyRunnable();
    23. //三个正式工处理任务
    24. pool.execute(target);
    25. pool.execute(target);
    26. pool.execute(target);
    27. //又来五个任务在门口等待,设置的任务队列最大为5
    28. pool.execute(target);
    29. pool.execute(target);
    30. pool.execute(target);
    31. pool.execute(target);
    32. pool.execute(target);
    33. //门口椅子坐不下了,开始请临时工
    34. //开始创建临时线程
    35. pool.execute(target);
    36. pool.execute(target);
    37. //人满了,拒绝营业
    38. //pool.execute(target);
    39. //关闭线程池(开发中一般不会使用)
    40. pool.shutdownNow(); //立即关闭,即使任务没有完成,会丢失任务
    41. pool.shutdown(); //任务跑完才关闭
    42. }
    43. }

    4、线程池处理Callable任务

    1. //Callable任务对象
    2. public class MyCallable implements Callable {
    3. public int n;
    4. public MyCallable() {
    5. }
    6. public MyCallable(int n) {
    7. this.n = n;
    8. }
    9. @Override
    10. public String call() throws Exception {
    11. int sum = 0;
    12. for (int i = 0; i < n; i++) {
    13. sum += i;
    14. }
    15. return Thread.currentThread().getName() + "执行1-" + n + "的和,输出结果为:" + sum;
    16. }
    17. }
    18. public class ThreadPoolDemo2 {
    19. public static void main(String[] args) throws Exception {
    20. //1、创建线程池对象
    21. ExecutorService pool = new ThreadPoolExecutor(3,5,6, TimeUnit.SECONDS,
    22. new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
    23. new ThreadPoolExecutor.AbortPolicy());
    24. //2、创建任务对象给线程池处理
    25. /*Callable call = new MyCallable(100);
    26. pool.submit(call);*/
    27. //submit可以接Future对象,接返回值
    28. //三个线程可以执行完成
    29. Future f1 = pool.submit(new MyCallable(100));
    30. Future f2 = pool.submit(new MyCallable(200));
    31. Future f3 = pool.submit(new MyCallable(300));
    32. //任务队列
    33. Future f4 = pool.submit(new MyCallable(400));
    34. Future f5 = pool.submit(new MyCallable(500));
    35. Future f6 = pool.submit(new MyCallable(600));
    36. Future f7 = pool.submit(new MyCallable(600));
    37. Future f8 = pool.submit(new MyCallable(600));
    38. //创建临时线程
    39. Future f9 = pool.submit(new MyCallable(600));
    40. //String s1 = f1.get();
    41. //取结果会等待任务执行完
    42. System.out.println(f1.get());
    43. System.out.println(f2.get());
    44. System.out.println(f3.get());
    45. System.out.println(f4.get());
    46. System.out.println(f5.get());
    47. System.out.println(f6.get());
    48. System.out.println(f7.get());
    49. System.out.println(f8.get());
    50. System.out.println(f9.get());
    51. }
    52. }

    5、Executors工具类创建线程池

    1. //底层源码
    2. public static ExecutorService newFixedThreadPool(int nThreads) {
    3. //创建指定正式工的方法中不会创建临时工
    4. //因为正式工一旦挂了立马就会有新的线程补上来,相当于不死线程,没有空闲时间
    5. return new ThreadPoolExecutor(nThreads, nThreads,
    6. 0L, TimeUnit.MILLISECONDS,
    7. new LinkedBlockingQueue());
    8. }
    1. public class ThreadExecutors {
    2. public static void main(String[] args) {
    3. //使用Executors工具类创建线程池
    4. ExecutorService pool = Executors.newFixedThreadPool(3);
    5. pool.execute(new MyRunnable());
    6. pool.execute(new MyRunnable());
    7. pool.execute(new MyRunnable());
    8. //线程池中只有三个线程,没有多余的线程处理,等前三个任务执行完再来执行
    9. //进入等待队列
    10. pool.execute(new MyRunnable());
    11. }
    12. }

    八、定时器

    1、Timer定时器

    1. public class TimerDemo1 {
    2. public static void main(String[] args) {
    3. //Timer定时器创建和使用
    4. //单线程任务
    5. Timer timer = new Timer();
    6. //调用方法,创建定时器
    7. //3秒之后开始执行,每隔两秒再执行一次。
    8. timer.schedule(new TimerTask() {
    9. @Override
    10. public void run() {
    11. System.out.println(Thread.currentThread().getName() + "执行了一次");
    12. }
    13. },3000,2000);
    14. }
    15. }

     2、ScheduleExecutorService创建定时器

    1. public class ScheduleExecutorSerciceDemo1 {
    2. public static void main(String[] args) {
    3. //创建ScheduleExecutorService线程池做定时器
    4. ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
    5. //开启定时任务
    6. //TimerTask实现了Runnable接口
    7. pool.scheduleAtFixedRate(new TimerTask() {
    8. @Override
    9. public void run() {
    10. System.out.println(Thread.currentThread().getName() + "执行了--AAA");
    11. }
    12. },2,3, TimeUnit.SECONDS);
    13. }
    14. }

    九、补充知识:并发并行、生命周期

     并发:一个CPU对多个进程服务,一次只服务一个进程,但是速度特别快,使得每个进程都被服务。

    并行 :CPU多个线程同时执行,并且每个线程可以服务多个进程,所有并行与并发同时进行。

    生命周期

     

    sleep状态的线程是不需要抢锁 ,一旦时间到会直接变成可运行的进程,因为它休眠之后不会将锁释放,休眠结束直接运行。

    wait状态,一旦进入就会将锁释放给别人,等待结束后得到锁再继续运行。

     

     

  • 相关阅读:
    【数学建模】Topsis法python代码
    Android本地数据存储(SP、SQLite、Room)
    Kafka从入门到精通02
    流程梳理有什么价值?如何建立高效的流程管理体系?
    Pandas 基础入门(一)
    TCP/IP协议详解
    电脑重装系统之后设置
    Vue Router的介绍
    全球创新创业数据国内版
    IMX6ULL移植篇-编译单个指定的设备树文件
  • 原文地址:https://blog.csdn.net/m0_56044262/article/details/125833043