什么是多线程

发布时间:2024年01月17日

什么是线程?

线程是一个程序内部的一条执行流程

程序中如果只有一条执行流程,那这个程序是单线程的程序

什么是多线程?

多线程是从软硬件上实现的多条执行流程的技术(多条线程由cpu负责调度执行)

多线程创建

多线程的创建方式一:继承Thread类


定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
创建MyThread类的对象
调用线程对象的start()方法启动线程(启动后还是执行run方法的)

优缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

多线程的注意事项
1、启动线程必须是调用start方法,不是调用run方法

直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
只有调用start方法才是启动一个新的线程执行
2、不要把主线程任务放在启动子线程之前

这样主线程一直是先跑完的,相当于是一个单线程的效果了

package com.cqh.demo01;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
// 1、子类继承Thread线程类
public class MyThread extends Thread{
    // 重写Thread类的 run方法
    @Override
    public void run() {
        // 描述线程执行任务
        for (int i = 0; i <=5 ; i++) {

            System.out.println("子线程= " + i);
        }

    }
}

package com.cqh.demo01;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) {
        // 3、创建一个MyThread线程类的对象代表一个线程
        Thread myThread = new MyThread();
        // 启动线程
        myThread.start();
        for (int i = 0; i <=5 ; i++) {

            System.out.println("主线程 = " + i);
        }
    }
}

多线程的创建方式二:实现Runnable接口


定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
创建MyRunnable任务对象
把MyRunnable任务对象交给Thread处理?

调用线程对象的start()方法启动线程

优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。

缺点:需要多一个Runnable对象。

package com.cqh.demo02;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
// 1、定义一个类实现runnable接口
public class MyRunnable implements Runnable{
    // 2、重写run方法
    @Override
    public void run() {

        // 线程执行的任务
        for (int i = 0; i <=5 ; i++) {
            System.out.println("子线程= " + i);
        }
    }
}
package com.cqh.demo02;


/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) {
        // 3、创建一个MyRunnable线程类的对象代表一个线程
        Runnable myRunnable = new MyRunnable();
        // 4、把任务对象交给线程对象处理
        // 启动线程
        new Thread(myRunnable).start();
        for (int i = 0; i <=5 ; i++) {

            System.out.println("主线程 = " + i);
        }
    }
}

?

?线程创建方式二的匿名 内部类 写法

可以创建Runnable的匿名内部类对象。
再交给Thread线程对象。
再调用线程对象的start()启动线程。

package com.cqh.demo03;

import com.cqh.demo01.MyThread;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式
         Runnable runnable = new Runnable(){
             @Override
             public void run() {
                 for (int i = 0; i <=5 ; i++) {

                     System.out.println("子线程 = " + i);
                 }
             }
         };
        // 2、把任务对象交给线程对象处理
        // 启动线程
        new Thread(runnable).start();
        for (int i = 0; i <=5 ; i++) {

            System.out.println("主线程 = " + i);
        }
    }
}

?

简化
package com.cqh.demo03;

import com.cqh.demo01.MyThread;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式
         Runnable runnable = new Runnable(){
             @Override
             public void run() {
                 for (int i = 0; i <=5 ; i++) {

                     System.out.println("子线程1 = " + i);
                 }
             }
         };
        // 2、把任务对象交给线程对象处理
        // 启动线程
       new Thread(runnable).start();
        // 简化1
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <=5 ; i++) {

                    System.out.println("子线程2 = " + i);
                }
            }
        }).start();

        // 简化2
        new Thread(()-> {
                for (int i = 0; i <=5 ; i++) {

                    System.out.println("子线程3 = " + i);
                }
        }).start();

        for (int i = 0; i <=5 ; i++) {

            System.out.println("主线程 = " + i);
        }
    }
}

?前两种线程创建方式都存在的一个问题
假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

解决

JDK50提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)

多线程的第三种创建方式:利用Callable接口、FutureTask类来实现

1、创建任务对象

定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
把Callable类型的对象封装成FutureTask (线程任务对象)

2、把线程任务对象交给Thread对象。

3、调用Thread对象的start方法启动线程
4、线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

package com.cqh.demo04;

import java.util.concurrent.Callable;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
// 创建一个类实现Callable接口
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n){
        this.n=n;
    }
    // 2、重写call方法
    @Override
    public String call() throws Exception {
        int sum=0;
        for (int i = 0; i <=n ; i++) {
            sum+=i;

        }
        return "线程求出了1-"+n+"的和为"+sum;
    }
}

package com.cqh.demo04;


import com.cqh.demo02.MyRunnable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3、创建一个Callable线程类的对象代表一个线程
        Callable<String> myCallable = new MyCallable(100);

        // 4、把callable对象封装成一个futureTask对象
        FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
        // 5、 把任务对象交给线程对象
        // 启动线程
        new Thread(stringFutureTask).start();
        // 6、获取线程执行完成后的结果
        String s = stringFutureTask.get();
        System.out.println("s = " + s);

    }
}

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强,可以在线程执行完毕后去获取线程执行的结果,
缺点:编码复杂一点。?


多线程常用方法

package com.cqh.demo05;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
// 1、子类继承Thread线程类
public class MyThread extends Thread{
    public MyThread(){

    }
    // 构造器方法为参数设置名字
    public MyThread(String name){
        super(name);
    }
    // 重写Thread类的 run方法
    @Override
    public void run() {
        // 描述线程执行任务
        Thread thread = Thread.currentThread();
        for (int i = 0; i <=5 ; i++) {

            if (i==5){
                try {
                    // 线程暂停5秒 再执行
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(thread.getName()+"子线程= " + i);
        }

    }
}

package com.cqh.demo05;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest01 {
    // main方法是由一条默认的主线程执行
    public static void main(String[] args) throws InterruptedException {
        // 3、创建一个MyThread线程类的对象代表一个线程
        Thread myThread = new MyThread();

        //设置线程名字
        myThread.setName("子1");
        // 启动线程
        myThread.start();
        // 获取线程名字
        System.out.println( myThread.getName());

        MyThread thread2 = new MyThread("子2");
        thread2.start();
        // 该线程执行完后继续执行其他线程
        thread2.join();

        // 那个线程执行它它就会得到哪个线程对象
        // 可以用这个方法获取主线程名称
        Thread thread = Thread.currentThread();
        thread.setName("不错的线程 ");
        for (int i = 0; i <=5 ; i++) {

            System.out.println( thread.getName()+"主线程 = " + i);
        }
    }
}

线程安全问题

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

取钱案例

具体实现代码

用的是普通java项目

package com.cqh.demo06;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class Account {
    private String carId;// 银行卡号
    private double money;// 余额

    public Account() {
    }

    public Account(String carId, double money) {
        this.carId = carId;
        this.money = money;
    }

    public String getCarId() {
        return carId;
    }

    public void setCarId(String carId) {
        this.carId = carId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "carId='" + carId + '\'' +
                ", money=" + money +
                '}';
    }

    public void drawMoney(double money) {
        Thread thread = Thread.currentThread();
        String name = thread.getName();

        // 1、判断余额是否足够
        if(this.money>=money){

            System.out.println(name+"来取钱"+money+"成功");
            this.money-=money;
            System.out.println(name+"取钱,余额剩余"+this.money);
        }else {
            System.out.println(name+"来取钱,余额不足");
        }

    }
}

package com.cqh.demo06;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class DrawThread extends Thread{


    private Account account;
    public DrawThread(Account account,String name) {
        super(name);
        this.account=account;

    }

    @Override
    public void run() {
        // 取钱

        account.drawMoney(10000);
    }
}
package com.cqh.demo06;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest {
    public static void main(String[] args) {

        // 1、创建账户对象,代表两个人的共享账户
        Account account = new Account("CardId-110",10000);
        // 2、创建两个线程分别代表小明、小红分别在同一个账户去钱

        new DrawThread(account,"小明").start();
        new DrawThread(account,"小红").start();
    }
}

线程同步?

解决线程安全问题

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来.

同步代码块

作用:把共享资源的核心代码给上锁,以保证线程安全

原理: 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

注意:?

修改?

效果?

锁对象的使用规范
建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码 (类名.class) 对象作为锁对象


同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行.

修改

效果

同步方法底层原理
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
如果方法是实例方法:同步方法默认用this作为的锁对象。
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。?

同步代码块与同步方法比较

范围上:
同步代码块锁的范围更小,同步方法锁的范围更大

lock锁

Lock锁是]DK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

线程通信

什么是线程通信?

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
线程通信的常见模型(生产者与消费者模型)
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据
注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产

需求:

3个生产者线程,负责生产包子,每个线程每次只脑生产1个包子放在来子上,2个消费者线程负责吃包子,每人每次只能从集予上产1个包子吃。

注意

上述方法应该使用当前同步锁对象进行调用

具体代码

package com.cqh.demo07;



import java.util.ArrayList;
import java.util.List;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class Desk {
    private List<String> list=new ArrayList<>();

    // 生产
    public synchronized void put() {
        try {
            String name = Thread.currentThread().getName();
            // 判断有没有包子
            if (list.size()==0){

                list.add(name+"做的肉包子");
                System.out.println(name+"做了一个肉包子");
                Thread.sleep(2000);
                // 唤醒别人 等待自己

                this.notifyAll();
                this.wait();
            }else {
                // 唤醒别人 等待自己
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 消费

    public synchronized void get() {
        try {
            String name = Thread.currentThread().getName();
            if (list.size()==1){
                System.out.println(name+"吃了" + list.get(0));
                list.clear();
                Thread.sleep(1000);
                this.notifyAll();
                this.wait();
            }else {
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.cqh.demo07;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadTest {
    public static void main(String[] args) {

        Desk desk = new Desk();
        // 创建3个生产者

        new Thread(()-> {

            while (true){
                desk.put();
            }

        },"厨师1").start();
        new Thread(()-> {
            while (true){
                desk.put();
            }
        },"厨师2").start();
        new Thread(()-> {
            while (true){
                desk.put();
            }
        },"厨师3").start();
        //创建2个消费者
        new Thread(()-> {

            while (true){
                desk.get();
            }

        },"消费者1").start();
        new Thread(()-> {
            while (true){
                desk.get();
            }
        },"消费者2").start();
    }
}

线程池?

什么是线程池?
线程池就是一个可以复用线程的技术。
不使用线程池的问题
?用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

JDK 5.0起提供了代表线程池的接口: ExecutorService。?

如何得到线程池对象??

方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象?

方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

参数一: corePoolSize:指定线程池的核心线程的数量
参数二:maximumPoolSize: 指定线程池的最大线程数量。
参数三: keepAliveTime: 指定临时线程的存活时间
参数四: unit: 指定临时线程存活的时间单位(秒、分、时、天)
参数五:workQueue: 指定线程池的任务队列。

参数六: threadFactory: 指定线程池的线程工厂

参数七:handler;指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)?

线程池的注意事项
1、临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2、什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

处理runnable任务?

?

?

package com.cqh.demo08;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 描述任务是做什么的
        System.out.println(Thread.currentThread().getName()+"-----" );
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

package com.cqh.demo08;

import java.util.concurrent.*;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        // 1、通过ThreadPoolExecutor创建一个线程池对象
         ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        MyRunnable myRunnable = new MyRunnable();
        poolExecutor.execute(myRunnable);// 核心线程使用
        poolExecutor.execute(myRunnable);// 核心线程使用
        poolExecutor.execute(myRunnable);// 核心线程使用
        poolExecutor.execute(myRunnable);
        poolExecutor.execute(myRunnable);
        poolExecutor.execute(myRunnable);
        poolExecutor.execute(myRunnable);
        // 到了临时线程的创建时机了
        poolExecutor.execute(myRunnable);
        poolExecutor.execute(myRunnable);
        // 到了临时线程的创建时机
        poolExecutor.execute(myRunnable);
       // poolExecutor.shutdown();// 等着线程池的任务全部执行完毕后,再关闭线程池
       // poolExecutor.shutdownNow();// 立即关闭线程池


    }

}

?处理Callable任务

package com.cqh.demo08;

import java.util.concurrent.Callable;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class MyCallable implements Callable<String> {

    private int n;
    public MyCallable(int n){

    }

    @Override
    public String call() throws Exception {
        int sum=0;
        for (int i = 0; i <=5 ; i++) {
            sum+=1;
        }
        return Thread.currentThread().getName()+"求出了1-"+n+"的和是"+sum;
    }
}

package com.cqh.demo08;

import java.util.concurrent.*;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadPoolTest1 {
    public static void main(String[] args) throws Exception {
        // 1、通过ThreadPoolExecutor创建一个线程池对象
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

        // 使用线程处理callable任务
        Future<String> submit1 = poolExecutor.submit(new MyCallable(100));
        Future<String> submit2 = poolExecutor.submit(new MyCallable(200));
        Future<String> submit3 = poolExecutor.submit(new MyCallable(300));
        Future<String> submit4 = poolExecutor.submit(new MyCallable(400));

        System.out.println(submit1.get());
        System.out.println(submit2.get());
        System.out.println(submit3.get());
        System.out.println(submit4.get());


    }
}

Executors工具类实现线程池

概念:

它是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

?

注意 : 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象?

package com.cqh.demo08;

import java.util.concurrent.*;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class ThreadPoolTest2 {
    public static void main(String[] args) throws Exception {
        // 1、通过Executors创建一个线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 使用线程处理callable任务
        Future<String> submit1 = executorService.submit(new MyCallable(100));
        Future<String> submit2 = executorService.submit(new MyCallable(200));
        Future<String> submit3 = executorService.submit(new MyCallable(300));
        Future<String> submit4 = executorService.submit(new MyCallable(400));

        System.out.println(submit1.get());
        System.out.println(submit2.get());
        System.out.println(submit3.get());
        System.out.println(submit4.get());


    }
}


计算密集型的任务:核心线程数=CPU的核数 + 1
I0集型的任务:核心程数量 = CPU核数 * 2

大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

并发和并行?

什么是进程?

正在运行的程序(软件)就是一个独立的进程

线程是属于进程的,一个进程中可以同时运行很多个线程

进程中的多个线程其实是并发和并行执行的

什么是并发?

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发

简单来说就是:

在同一个时刻上,同时有多个线程在被CPU调度执行

线程的生命周期

也就是线程从生到死的过程中,经历的各种状态及状态转换。
理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态
Java总共定义了6种状态
6种状态都定义在Thread类的内部枚举类中

?线程的6种状态互相转换

?

悲观锁和乐观锁

?悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。 线程安全,性能较差!

乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

悲观锁案例

乐观锁

package com.cqh.demo09;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class MyRunnable2 implements Runnable {
   // 整数修改的乐观锁:原子类
    private AtomicInteger count=new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i <=100 ; i++) {

            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"count = " + (count.incrementAndGet()));
            }

        }

    }


}

?练习题

目标: .有100份礼品,小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出, 利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物。

package com.cqh.demo10;

import java.util.List;
import java.util.Random;


/**
 * @author cqh
 * @date 2024/1/17
 * @Description
 */
public class SendThread extends Thread {
    private List<String> gift;

    private String name;
    private int count=0;

    public SendThread(List<String> gift, String name) {
        super(name);
        this.gift = gift;
        this.name = name;
    }

    @Override
    public void run() {

        Random random = new Random();
        while (true) {
            if (gift.size() < 10) {
                return;
            }

            synchronized (gift) {
                String remove = gift.remove(random.nextInt(gift.size()));
                System.out.println(name+"发出了礼物" + remove);
                count++;
            }


        }

    }




    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}
package com.cqh.demo10;

import jdk.nashorn.internal.ir.CallNode;

import java.util.ArrayList;
import java.util.Random;

/**
 * @author cqh
 * @date 2024/1/16
 * @Description
 */
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        // 目标: .有100份礼品,小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,
// 利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物。
        ArrayList<String> gift = new ArrayList<>();
        String[] names={"苹果手机","笔记本电脑","ipad","耳机","音响"};
        Random random = new Random();
        for (int i = 0; i <100 ; i++) {
            gift.add(names[random.nextInt(names.length)]+(i+1));
        }

        SendThread sendThread1 = new SendThread(gift,"小明");
        SendThread sendThread2 = new SendThread(gift, "小红");

        sendThread1.start();
        sendThread1.join();
        sendThread2.start();
        sendThread2.join();

        System.out.println(sendThread1.getName() +"发送了"+ sendThread1.getCount()+"个礼物");
        System.out.println(sendThread2.getName() +"发送了"+ sendThread2.getCount()+"个礼物");
    }
}

文章来源:https://blog.csdn.net/qq_67832732/article/details/135623301
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。