• JUC并发编程系列详解篇四(线程基础理论)


    进程与线程

    1、线程是进程中的一个实体,线程本身是不会独立存在的 。
    2、进程是代码在数据集合上的一次运行活动 , 是系统进行资源分配和调度的基本单位 , 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
    3、系统运行一个程序即是一个进程从创建,运行到消亡的过程。
    4、在 Java 中,当我们启动 main 函数时其实就启动了一个 jvm 的进程, 而main 函数所在的线程是这个进程中的一个线程,也称主线程 。

    实例代码:

    import java.lang.management.ManagementFactory;
    import java.lang.management.ThreadInfo;
    import java.lang.management.ThreadMXBean;
    
    /**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/22 15:20
     */
    public class MultithThreadTest {
        public static void main(String[] args) {
            ThreadMXBean bean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] infos = bean.dumpAllThreads(false, false);
            for (ThreadInfo threadInfo: infos){
                System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结果(不同的电脑可能有不同的输出):
    在这里插入图片描述

    线程的基本状态

    Java线程运行的生命周期中只可能是6种不同的状态之一,在给定的一个时刻,线程只能处于其中的一个状态。

    1、New状态:初始状态,线程被构建出来,但是还没有调用start()方法
    2、 Runnable状态:运行状态,java线程把操作系统的就绪和运行两种状态统称为“运行中”。
    3、 Blocked状态:阻塞状态,表示线程阻塞与锁。
    4、 Waiting状态:等待状态,表示线程进入等待状态,进入该状态表示线程需要等待其他线程做出一些特定的动作或通知。
    5、TIME WAITING状态:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
    6、 TIME WAITING状态:终止状态,表示当前线程已经执行完毕

    Java线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图所示:
    在这里插入图片描述

    线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

    实例测试:

    public class ThreadState {
        public static void main(String[] args) {
            new Thread(new TimeWaiting(),"TimeWaitingThread").start();
            new Thread(new Waiting(),"WaitingThread").start();
    
            new Thread(new Blocked(),"Blocked-1").start();
            new Thread(new Blocked(),"Blocked-2").start();
        }
        // 这个线程不断地休眠
        static class TimeWaiting implements Runnable{
            @Override
            public void run() {
                while (true){
                    SleepUtils.second(2000);
                }
            }
        }
        // 这个线程在Waiting.Class上等待
        static class Waiting implements Runnable{
            @Override
            public void run() {
                while (true){
                    synchronized (Waiting.class){
                        try {
                            Waiting.class.wait();
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        // 该线程在Blocked.class实例上加锁后,不会释放该锁
        static class Blocked implements Runnable{
            @Override
            public void run() {
                synchronized (Blocked.class){
                    while (true){
                        SleepUtils.second(1000);
                    }
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    代码2(放在同一个文件夹下运行)

    import java.util.concurrent.TimeUnit;
    
    public class SleepUtils {
        public static final void second(long second){
            try {
                TimeUnit.SECONDS.sleep(second);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行代码,打开终端或者命令提示符,键入“jps”,输出如下:
    在这里插入图片描述
    然后就可以看到运行实例对应的进程ID是177428,然后在命令行终端输入jstack 17748,部分终端输出如下:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    线程的几种创建方式

    在这里插入图片描述

    通过Thread类创建多线程

    继承Thread类实现多线程有两种方法,一种是当做线程对象存在实现(也就是继承Thread类,并实现Thread对象),第二种是通过匿名内部类实现多线程。

    作为线程对象存在(继承Thread对象)

    	/**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/25 20:50
     */
    public class CreateThreadTest01 extends Thread{
        public CreateThreadTest01(String CreateName){
            super(CreateName);
        }
        @Override
        public void run(){
            while (!interrupted()){
                System.out.println(getName()+"线程执行了......");
                try{
                    Thread.sleep(2000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            CreateThreadTest01 test01 = new CreateThreadTest01("Name1");
            CreateThreadTest01 test02 = new CreateThreadTest01("Name2");
            test01.start();
            test02.start();
            //test01.interrupt();//中断线程1
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结果:可以看出线程1和线程2不间断地交替打印。
    在这里插入图片描述
    当取消掉调用 interrupted方法的注释,可以用来判断该线程是否被中断。(终止线程不允许用stop方法,该方法不会施放占用的资源)。当使用了中断线程时,结果如下:线程1已经不能运行了,只能打印线程2.
    在这里插入图片描述

    匿名内部类创建线程对象

    /**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/25 20:59
     */
    public class CreateThreadTest03{
        public static void main(String[] args) {
            new Thread(){
                @Override
                public void run(){
                    System.out.println("无参数线程创建成功!");
                }
            }.start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("带线程任务的线程对象执行了");
                }
            }).start();
            // 创建带线程任务并且重写的run方法的线程对象
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("runnable run 线程执行了。。。。");
                }
            }){
                @Override
                public void run(){
                    System.out.println("Override run 执行了");
                }
            }.start();
            // 调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    结果:
    在这里插入图片描述

    实现runnable接口,作为线程任务存在

    Runnable 接口只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。
    实例代码:

    import static java.lang.Thread.interrupted;
    
    /**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/25 20:55
     */
    public class CreateThreadTest02 implements Runnable{
        @Override
        public void run() {
            while (!interrupted()){
                System.out.println("线程执行了......");
                try{
                    Thread.sleep(2000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            // 将线程任务传递给线程对象
            Thread thread = new Thread(new CreateThreadTest02());
            //启动线程
            thread.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    结果: 程序每隔两秒打印一次
    在这里插入图片描述

    利用接口Callable创建带返回值的线程

    实例代码:

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.Future;
    import java.util.concurrent.FutureTask;
    
    /**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/25 21:01
     */
    
    public class CreateThreadTest04 implements Callable {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CreateThreadTest04 teat04 = new CreateThreadTest04();
            // 最终实现的是Runnable的接口
            FutureTask<String> task = new FutureTask<String>(teat04);
            Thread thread = new Thread(task);
            thread.start();
            System.out.println("哈哈哈哈哈");
            String str = task.get();
            System.out.println("在线程的结果是:"+str);
        }
        @Override
        public Object call() throws Exception {
            Thread.sleep(2000);
            return "ABCD";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结果:
    在这里插入图片描述

    使用定时器(Timer)

    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * @author: 随风飘的云
     * @describe:
     * @date 2022/03/25 21:04
     */
    public class CreateThreadTest05 {
        public static void main(String[] args) {
            Timer timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("定时器的线程执行了......");
                }
            },0,1000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    结果:
    在这里插入图片描述

    线程池创建线程

    线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

    停止线程池

    shutdown
    isShutdown:是否停止的状态,开始运行就是true
    isTerminated:线程是否完全停止
    awaitTerminated:在一定时间内线程是否停止 返回boolean
    stutdownNow:立即停止,返回被中断的列表。

    线程池的状态

    RUNNING:接受新任务并处理排第任务。
    SHUTDOWN:不接受新的任务,但处理排队任务。
    STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务。
    TIDYING:所有任务都已终止,workCont为零时,线程会转换到TIDTYING状态,并将运行terminate()的钩子方法。
    TERMINATED:terminate()运行完成。

    使用newSingleThreadExecutor创建线程

    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    /**
     * @description 线程池创建:newSingleThreadExecutor()单一线程池
     *                        跟newFixedThreadPool()原理一样,只是把线程数和最大线程数设置为1
     */
    public class SingleThreadExecutor {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                executorService.execute(new FixedThreadPoolExecutor.Task());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

    import java.util.concurrent.Executor;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    public class CreateThreadTest06 {
        public static void main(String[] args) {
            // 创建具有5个线程的线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(5);
            long threadPoolStart= System.currentTimeMillis();
            for (int i = 0; i < 5; i++) {
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+"线程执行了......");
                    }
                });
            }
            long threadPoolEnd = System.currentTimeMillis();
            System.out.println("创建5个线程用时:"+(threadPoolEnd-threadPoolStart));
            // 销毁线程池
            threadPool.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    使用newFixedThreadPool创建线程

    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    /**
     * @description 线程池创建:newFixedThreadPool()自定义线程数线程池
     *              由于传进去的LinkedBlockingQueue无界队列是没有队列上限的,所以当请求越来越多,并且无法及时处理完毕的时候,
     *              也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。
     */
    public class FixedThreadPoolExecutor {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Task());
            }
            // 手动关闭线程
            executorService.shutdown();
        }
        static class Task implements Runnable{
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    使用newCachedThreadPool创建线程

    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    /**
     * @description 线程池创建:newCachedThreadPool()可缓存线程池
     *                        特点:SynchronousQueue无界线程池,具有自动回收多余线程的功能
     *                        缺点:maxPoolSize是Integer.MAX_VALUE,可能会创建数量非常多线程,甚至导致OOM。
     */
    public class CachedThreadPoolExecutor {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                executorService.execute(new FixedThreadPoolExecutor.Task());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    newScheduledThreadPool创建线程

    创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

    /**
     * @description 线程池创建:newScheduledThreadPool() 支持定时及周期性任务执行的线程池
     */
    public class ScheduledThreadPoolExecutor {
        public static void main(String[] args) throws InterruptedException {
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
            //scheduledExecutorService.schedule(new FixedThreadPoolExecutor.Task(), 1, TimeUnit.SECONDS);
            // 每隔一秒一执行
            scheduledExecutorService.scheduleAtFixedRate(new FixedThreadPoolExecutor.Task(), 1 , 1, TimeUnit.SECONDS);
            Thread.sleep(5000);
            scheduledExecutorService.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    停止运行的线程

    线程在运行的过程中退出有如下几种方法:

    1、使用退出标志,使得线程正常退出,就是说当run方法执行完毕后线程终止。
    2、使用stop方法强制退出,但是不推荐这个方法
    3、使用interrupt方法中断线程
    4、程序运行结束,线程自动结束。

    public class ThreadTest extends Thread{
        volatile boolean stop = false;
        public void run(){
            while (!stop){
                System.out.println(getName()+"线程正在运行中......");
                try{
                    sleep(1000);
                }catch (InterruptedException e){
                    System.out.println("wake up from block ......");
                    stop = true;
                    e.printStackTrace();
                }
            }
            System.out.println(getName()+"线程已经退出了......");
        }
    }
    class InterruptThreadDemo {
        public static void main(String[] args) throws InterruptedException {
            ThreadTest threadTest = new ThreadTest();
            System.out.println("线程开始运行......");
            threadTest.start();
            Thread.sleep(3000);
            System.out.println("出现Interrupt的线程为:"+threadTest.getName());
            threadTest.stop = true;
            threadTest.interrupt();
            Thread.sleep(3000);
            System.out.println("主线程已经停止了......");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    运行结果为:
    在这里插入图片描述

    线程的优先级

    现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

    Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

    public class Priority {
    	private static volatile boolean notStart = true;
    	private static volatile boolean notEnd = true;
    public static void main(String[] args) throws Exception {
    	List<Job> jobs = new ArrayList<Job>();
    	for (int i = 0; i < 10; i++) {
    		int priority = i < 5 Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
    		Job job = new Job(priority);
    		jobs.add(job);
    		Thread thread = new Thread(job, "Thread:" + i);
    		thread.setPriority(priority);
    		thread.start();
    	}
    	notStart = false;
    	TimeUnit.SECONDS.sleep(10);
    	notEnd = false;
    	for (Job job : jobs) {
    		System.out.println("Job Priority : " + job.priority + ",
    			Count : " + job.jobCount);
    	}
    }
    static class Job implements Runnable {
    	private int priority;
    	private long jobCount;
    	public Job(int priority) {
    		this.priority = priority;
    	}
    	public void run() {
    		while (notStart) {
    			Thread.yield();
    		}
    		while (notEnd) {
    			Thread.yield();
    			jobCount++;
    		}
    	}
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    扩展阅读

    Runnable接口和Callable接口的区别

    首先查看一下Runnable接口的源代码,反正Runnable接口中只有一个方法,Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
    在这里插入图片描述
    其次再查看一下Callable接口中的源码,Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
    在这里插入图片描述
    这其实是很有意义的,首先多线程的运行和单线程运行相比有很多复杂困难的问题,因为多线程运行充满了未知性,比如说某条线程是否运行,运行了多少时间,线程所期望的数据是否已经赋值完毕,Callable接口的泛型的Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。这是一个优势。

    start()方法和run()方法的区别

    每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动⼀一个线程;

    start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态;

    run()方法称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。

    sleep()方法和wait()方法的区别

    首先sleep()方法可以在任何地方使用,而wait()方法只能在同步方法或者同步块中运行。对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程(其他CPU可以执行其他任务),但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。注意在调用 sleep()方法的过程中, 线程不会释放对象锁。当调用 wait()方法的时候,当前线程会暂时退出同步资源锁,同时会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

  • 相关阅读:
    SAP 通过 SAT 查找增强 (实例 :AS01/AS02/AS03屏幕增强新增页签或字段)<转载>
    CentOS 7 服务器上创建新用户及设置用户密码有效期
    拿下跨界C1轮投资,本土Tier 1高阶智能驾驶系统迅速“出圈”
    【Android面试八股文】性能优化相关面试题: 什么是内存抖动?什么是内存泄漏?
    uni-data-picker 级联选择器只 显示最后一个
    MySQL基础
    git diff 命令
    注册树模式
    【English】十大词性之感叹词(感叹句)
    【CSS动效实战(纯CSS与JS动效)】02 flex 布局实战(仿 JD 及 gitCode 布局)及 media 自适应初探
  • 原文地址:https://blog.csdn.net/m0_46198325/article/details/126796175