• 多线程篇1:java创建多线程以及线程状态


    一、概述

    1、多任务

    当我们打开电脑,可以一边打开qq音乐听歌,一边打开浏览器浏览网页,还算可以上qq聊天。电脑是同时可以执行多个任务的,

    CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

    例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:

    这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样

    类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。

    2、进程和线程

    计算中,把一个任务称为一个进程,如上面的qq是一个进程,浏览器也是一个进程,每个子任务称作一个线程,比如qq聊天打字的同时也可以接收消息,就是两个子任务即两个线程。

    进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程

    操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

    因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

    • 使用多进程
    • 使用单进程多线程
    • 使用多进程+多线程

    具体采用哪种方式,要考虑到进程和线程的特点。

    多线程相比,多进程的缺点在于:

    • 创建进程比创建线程开销大,尤其是在Windows系统上;
    • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

    而多进程的优点在于:

    多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

    3、多线程

    Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

    因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

    和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

    Java多线程编程的特点又在于:

    • 多线程模型是Java程序最基本的并发模型;
    • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

    4、用户线程 守护线程

    Java中线程分为用户线程和守护线程两种。用户线程是用户自定义的线程,当主线程停止用户线程不会停止,守护线程当进程不存在或者主线程停止,守护线程也会停止,通过setDaemon(true)将一个线程设置为守护线程

    5、什么是JUC

    在 Java 中, 线程部分是一个重点, 本篇文章说的 J UC 也是关于线程的。 J UC 就是 java.util . concurrent 工具包的简称。 这是一个处理线程的工具包, JDK 1 . 5 开始出现的。

    6、串行、并发、并行

    6.1、概念

    串行:串行是一次只能取得一个任务,并执行这个任务

    并发:指一个处理器同时处理多个任务。(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)

    并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

    并行,是每个cpu运行一个程序。

    6.2、案例

    1、并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。

    2、多个人同时做一件事 ,多个人同时做不同的事

    二、多线程创建

    1、继承Thread类

    public class Test {
        public static void main(String[] args) {
            new MyThread().start();
            for (int i = 0; i < 100; i++) {
                String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(),
                        Thread.currentThread().getThreadGroup().getName(),i);
                System.out.println(log);
            }
        }
    }
    class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    2、实现Runbable接口

    public class Test {
        public static void main(String[] args) {
            //也可以直接使用lamda表达式
            Thread thread = new Thread(new MyThread(),"myThread");
            thread.start();
            for (int i = 0; i < 100; i++) {
                String log = String.format("线程%s(属于线程组%s)打印%d",Thread.currentThread().getName(),
                        Thread.currentThread().getThreadGroup().getName(),i);
                System.out.println(log);
            }
        }
    }
    class MyThread implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    Thread类常用的方法

    currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;

    start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;

    yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿 意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield() ⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;

    sleep():静态⽅法,使当前线程睡眠⼀段时间;

    Thread.setPriority(int n) // 1~10, 默认值5 可以对线程设定优先级 ,优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

    两种方式比较:

    由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。

    Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。

    Runnable接⼝出现,降低了线程对象和线程任务的耦合性。 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量。

    所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类

    三、线程状态

    1、线程状态概述

    在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

    • New:新创建的线程,尚未执行;
    • Runnable:运行中的线程,正在执行run()方法的Java代码;
    • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
    • Waiting:运行中的线程,因为某些操作在等待中;
    • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
    • Terminated:线程已终止,因为run()方法执行完毕。

    其实java中关于线程状态在thread类是有一个枚举的,如下

    public enum State {
     NEW,
     RUNNABLE,
     BLOCKED,
     WAITING,
     TIMED_WAITING,
     TERMINATED;
    }
    

    用一个状态转移图表示如下:

    1. ┌─────────────┐
    2. │ New │
    3. └─────────────┘
    4. ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
    5. ┌─────────────┐ ┌─────────────┐
    6. ││ Runnable │ │ Blocked ││
    7. └─────────────┘ └─────────────┘
    8. │┌─────────────┐ ┌─────────────┐│
    9. │ Waiting │ │Timed Waiting│
    10. │└─────────────┘ └─────────────┘│
    11. ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
    12. ┌─────────────┐
    13. │ Terminated │
    14. └─────────────┘

    当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

    线程终止的原因有:

    • 线程正常终止:run()方法执行到return语句返回;
    • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
    • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

    一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                System.out.println("hello");
            });
            System.out.println("start");
            t.start();
            t.join();
            System.out.println("end");
        }
    }

    main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印startt线程再打印hellomain线程最后再打印end

    如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

    2、具体转换

    线程之间的具体转成如下表示

    de00cfaaa8807cf5f320cff05f1d6bfc.png

    NEW

    处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法

    private void testStateNew() {
     Thread thread = new Thread(() -> {});
     System.out.println(thread.getState()); // 输出 NEW 
    }

    从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。

    关于start()的两个引申问题

    1. 反复调⽤同⼀个线程的start()⽅法是否可⾏?

    2. 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程 的start()⽅法是否可⾏?

    public synchronized void start() {
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
            group.add(this);
    
            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                   
                }
            }
        }

    我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。

    我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:

    @Test
    public void testStartMethod() {
     Thread thread = new Thread(() -> {});
     thread.start(); // 第⼀次调⽤
     thread.start(); // 第⼆次调⽤
    }

    我是在start()⽅法内部的最开始打的断点,叙述下在我这⾥打断点看到的结果: 第⼀次调⽤时threadStatus的值是0。 第⼆次调⽤时threadStatus的值不为0。 查看当前线程状态的源码:

    // Thread.getState⽅法源码:
    public State getState() {
     // get current thread state
     return sun.misc.VM.toThreadState(threadStatus);
    }
    // sun.misc.VM 源码:
    public static State toThreadState(int var0) {
     if ((var0 & 4) != 0) {
     return State.RUNNABLE;
     } else if ((var0 & 1024) != 0) {
     return State.BLOCKED;
     } else if ((var0 & 16) != 0) {
     return State.WAITING;
     } else if ((var0 & 32) != 0) {
     return State.TIMED_WAITING;
     } else if ((var0 & 2) != 0) {
     return State.TERMINATED;
     } else {
     return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
     }
    }
    

    两个问题的答案都是不可⾏,在调⽤⼀次start()之后,threadStatus的值会改 变(threadStatus !=0),此时再次调⽤start()⽅法会抛出 IllegalThreadStateException异常。 ⽐如,threadStatus为2代表当前线程状态为TERMINATED。

    RUNNABLE

    表示当前线程正在运⾏中。处于RUNNABLE状态的线程在Java虚拟机中运⾏,也 有可能在等待其他系统资源(⽐如I/O)。

    Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和 running两个状态的。

    BLOCKED

    阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进⼊同步区。 我们⽤BLOCKED状态举个⽣活中的例⼦:

    假如今天你下班后准备去⻝堂吃饭。你来到⻝堂仅有的⼀个窗⼝,发现前⾯ 已经有个⼈在窗⼝前了,此时你必须得等前⾯的⼈从窗⼝离开才⾏。 假设你是线程t2,你前⾯的那个⼈是线程t1。此时t1占有了锁(⻝堂唯⼀的 窗⼝),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。

    WAITING

    等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。 调⽤如下3个⽅法会使线程进⼊等待状态: Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;

    Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;

    LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。

    你等了好⼏分钟现在终于轮到你了,突然你们有⼀个“不懂事”的经理突然来 了。你看到他你就有⼀种不祥的预感,果然,他是来找你的。 他把你拉到⼀旁叫你待会⼉再吃饭,说他下午要去作报告,赶紧来找你了解 ⼀下项⽬的情况。你⼼⾥虽然有⼀万个不愿意但是你还是从⻝堂窗⼝⾛开 了。 此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗 ⼝)了,“不速之客”来了你还是得释放掉锁。此时你t2的状态就是 WAITING。然后经理t1获得锁,进⼊RUNNABLE状态。 要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能⼀直等待 了。

    TIMED_WAITING

    超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。 调⽤如下⽅法会使线程进⼊超时等待状

    1、Thread.sleep(long millis):使当前线程睡眠指定时间

    2、Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;

    3、Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则 会⼀直执⾏;

    4、LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线 程进⾏线程调度指定时间

    5、LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时 间;

    到了第⼆天中午,⼜到了饭点,你还是到了窗⼝前。 突然间想起你的同事叫你等他⼀起,他说让你等他⼗分钟他改个bug。 好吧,你说那你就等等吧,你就离开了窗⼝。很快⼗分钟过去了,你⻅他还 没来,你想都等了这么久了还不来,那你还是先去吃饭好了。 这时你还是线程t1,你改bug的同事是线程t2。t2让t1等待了指定时间,t1先 主动释放了锁。此时t1等待期间就属于TIMED_WATING状态。 t1等待10分钟后,就⾃动唤醒,拥有了去争夺锁的资格。

    TERMINATED

    终⽌状态。此时线程已执⾏完毕。

    四 、线程中断

    当执行一个很耗时的任务时,比如下载文件,用户随时可能取消下载,当前取消下载,我们应在服务端中断当前下载文件的线程。

    中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

    中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

    1. public class Main {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t = new MyThread();
    4. t.start();
    5. Thread.sleep(1); // 暂停1毫秒
    6. t.interrupt(); // 中断t线程
    7. t.join(); // 等待t线程结束
    8. System.out.println("end");
    9. }
    10. }
    11. class MyThread extends Thread {
    12. public void run() {
    13. int n = 0;
    14. while (! isInterrupted()) {
    15. n ++;
    16. System.out.println(n + " hello!");
    17. }
    18. }
    19. }

    仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

    如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

    1. public class Main {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t = new MyThread();
    4. t.start();
    5. Thread.sleep(1000);
    6. t.interrupt(); // 中断t线程
    7. t.join(); // 等待t线程结束
    8. System.out.println("end");
    9. }
    10. }
    11. class MyThread extends Thread {
    12. public void run() {
    13. Thread hello = new HelloThread();
    14. hello.start(); // 启动hello线程
    15. try {
    16. hello.join(); // 等待hello线程结束
    17. } catch (InterruptedException e) {
    18. System.out.println("interrupted!");
    19. }
    20. hello.interrupt();
    21. }
    22. }
    23. class HelloThread extends Thread {
    24. public void run() {
    25. int n = 0;
    26. while (!isInterrupted()) {
    27. n++;
    28. System.out.println(n + " hello!");
    29. try {
    30. Thread.sleep(100);
    31. } catch (InterruptedException e) {
    32. break;
    33. }
    34. }
    35. }
    36. }

     

    main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

    另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

    1. public class Main {
    2. public static void main(String[] args) throws InterruptedException {
    3. HelloThread t = new HelloThread();
    4. t.start();
    5. Thread.sleep(1);
    6. t.running = false; // 标志位置为false
    7. }
    8. }
    9. class HelloThread extends Thread {
    10. public volatile boolean running = true;
    11. public void run() {
    12. int n = 0;
    13. while (running) {
    14. n ++;
    15. System.out.println(n + " hello!");
    16. }
    17. System.out.println("end!");
    18. }
    19. }

    注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

    为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

    1. ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
    2. Main Memory
    3. │ │
    4. ┌───────┐┌───────┐┌───────┐
    5. │ │ var A ││ var B ││ var C │ │
    6. └───────┘└───────┘└───────┘
    7. │ │ ▲ │ ▲ │
    8. ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
    9. │ │ │ │
    10. ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
    11. ▼ │ ▼ │
    12. │ ┌───────┐ │ │ ┌───────┐ │
    13. var A │ │ var C │
    14. │ └───────┘ │ │ └───────┘ │
    15. Thread 1 Thread 2
    16. └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘

    这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

    因此,volatile关键字的目的是告诉虚拟机:

    • 每次访问变量时,总是获取主内存的最新值;
    • 每次修改变量后,立刻回写到主内存。

    volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

    如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

    小结

    对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException

    目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;

    通过标志位判断需要正确使用volatile关键字;

    volatile关键字解决了共享变量在线程间的可见性问题;

    五、守护线程

    Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

    如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

    但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程

    1. class TimerThread extends Thread {
    2. @Override
    3. public void run() {
    4. while (true) {
    5. System.out.println(LocalTime.now());
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. break;
    10. }
    11. }
    12. }
    13. }

    如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?

    然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

    答案是使用守护线程(Daemon Thread)。

    守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

    因此,JVM退出时,不必关心守护线程是否已结束。

    如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

    1. Thread t = new MyThread();
    2. t.setDaemon(true);
    3. t.start();

    在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

    小结

    守护线程是为其他线程服务的线程;

    所有非守护线程都执行完毕后,虚拟机退出;

    守护线程不能持有需要关闭的资源(如打开文件等);

    参考

    https://www.liaoxuefeng.com/wiki/1252599548343744/1306580767211554

  • 相关阅读:
    (动态)树分治 学习笔记
    多态的讲解
    今天的码农女孩学习了关于ajax技术
    排序算法概述
    抖音小店无货源怎么操作?针对新手小白最新玩法分享,记得收藏
    数据结构 【树状数组】【线段树】【珂朵莉树】
    cola 架构简单记录
    Lambda 表达式:解锁编程世界的魔法之门
    OpenHarmony Trace的使用
    SPI接口协议的学习3
  • 原文地址:https://blog.csdn.net/qq_34491508/article/details/126214023