重学java基础----多线程

参考于遇见狂神说视频以及学习笔记

核心概念

在这里插入图片描述

线程创建的方式(重要

在这里插入图片描述

//方式一:继承Thread类,重写run()方法,调用start开启线程
// 线程开启不一定立即执行,由cpu调度执行

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/14-15:41
 */
//方式一:继承Thread类,重写run()方法,调用start开启线程
// 线程开启不一定立即执行,由cpu调度执行

public class TestThread1 extends  Thread{
    
    
    @Override
    public void run() {
    
    
        //线程体
        for (int i = 200; i > 0; i--) {
    
    
            System.out.println("我在学习多线程"+i);
        }
    }

    public static void main(String[] args) {
    
    
        //主线程

        //创建一个线程对象
        TestThread1 testThread1 =new TestThread1();

        //调用start方法开启线程
        testThread1.start();

        for (int i = 1000; i > 0; i--) {
    
    
            System.out.println("我在看代码"+i);
        }
    }
}

实现runnable接口,重写run方法,创建线程对象,执行线程丢入runnable接口实现类,调用start方法

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/14-15:58
 */
//方式2:实现runnable接口,重写run方法,创建线程对象,执行线程丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        //线程体
        for (int i = 200; i > 0; i--) {
    
    
            System.out.println("我在学习多线程"+i);
        }
    }

    public static void main(String[] args) {
    
    
        //主线程
        TestThread2 testThread2 =new TestThread2();

        new Thread(testThread2).start();

        for (int i = 1000; i > 0; i--) {
    
    
            System.out.println("我在看代码"+i);
        }
    }
}

两种方式总结

在这里插入图片描述

实现callable接口,开启多线程

package com.Thread;

import java.util.concurrent.*;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/14-16:28
 */
public class TestThread4 implements Callable<Boolean> {
    
    

    @Override
    public Boolean call() throws Exception {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println(Thread.currentThread().getName()+" i="+i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        TestThread4 testThread1 =new TestThread4();
        TestThread4 testThread2 =new TestThread4();
        TestThread4 testThread3 =new TestThread4();

        //创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);
        //提交执行:
        Future<Boolean> result1 = ser.submit(testThread1);
        Future<Boolean> result2 = ser.submit(testThread2);
        Future<Boolean> result3 = ser.submit(testThread3);
        //获取结果:
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        boolean r3 = result3.get();
        //关闭服务:
        ser.shutdownNow();

    }
}

使用runnable接口用到了静态代理的知识,
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色 使用静态代理的好处:

  1. 代理对象可以做很多真实对象无法做的事情
  2. 真实对象专注于做自己的事情
package com.Thread;

import javax.activation.MailcapCommandMap;
import java.util.Map;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/14-16:45
 */
//静态代理模式
public class staticTest {
    
    

    public static void main(String[] args) {
    
    
        weddingCompany weddingCompany =new weddingCompany(new You());

        weddingCompany.HappyMarry();

    }
}


interface  Marry{
    
    
    void HappyMarry();
}
class You implements Marry{
    
    

    @Override
    public void HappyMarry() {
    
    
        System.out.println("小明要结婚了,很开心");
    }
}

class weddingCompany implements Marry{
    
    

    private Marry target;

    public weddingCompany(Marry target){
    
    
        this.target =target;
    }

    @Override
    public void HappyMarry() {
    
    
        before();
        this.target.HappyMarry();
        after();
    }

    private void after() {
    
    
        System.out.println("结婚后,收拾现场");
    }

    private void before() {
    
    
        System.out.println("结婚前,布置现场");
    }
}

线程的5大状态

在这里插入图片描述
在这里插入图片描述

常用方法

在这里插入图片描述

如何停止线程

1.建议线程正常停止—》利用次数,不建议死循环
2.建议使用标志位—》设置一个标志位
3.不要使用stop或destroy等过时或jdk不建议使用的方法

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-8:33
 * 测试stop
 1.建议线程正常停止---》利用次数,不建议死循环
 2.建议使用标志位---》设置一个标志位
 3.不要使用stop或destroy等过时或jdk不建议使用的方法
 *
 */


public class Thread5 implements  Runnable {
    
    

    //1.设置一个标志位
    private boolean flag=true;

    @Override
    public void run() {
    
    
      int i=0;
      while (flag){
    
    
          System.out.println("run...thread"+i++);
      }
    }

    //2.设置一个公开的方法停止线程,转换标志位
    public void stop(){
    
    
        this.flag=false;
    }

    public static void main(String[] args) {
    
    
        Thread5 thread5=new Thread5();
        new Thread(thread5).start();

        //调用stop方法切换标志位,让线程停止
        for (int i = 1; i <=1000; i++) {
    
    
            System.out.println("main..."+i);
            if (i == 900) {
    
    
                thread5.stop();
                System.out.println("线程停止了。。。");
            }
        }
    }
}

线程休眠

在这里插入图片描述

  • sleep()方法的用处:
  • 1.模拟网络延迟,放大线程中的并发问题
  • 2.模拟倒计时
  • 3.获取当前系统时间
  • 模拟倒计时
 package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-8:50
 *
 * sleep()方法的用处:
 * 1.模拟网络延迟,放大线程中的并发问题
 * 2.模拟倒计时
 * 3.获取当前系统时间
 */
public class TestSleep {
    
    

    public static void main(String[] args) {
    
    
        try {
    
    
            testDown();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

    }


    public static void testDown() throws InterruptedException {
    
    
        int num=10;
        while (true) {
    
    
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0){
    
    
               break;
            }

        }
    }
}
  • 打印当前系统时间

package com.Thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-8:50
 *
 * sleep()方法的用处:
 * 1.模拟网络延迟,放大线程中的并发问题
 * 2.模拟倒计时
 * 3.获取当前系统时间
 */

public class TestSleep {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        //打印当前系统时间
        Date date=new Date(System.currentTimeMillis());//获取系统当前时间
        while (true) {
    
    
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            date=new Date(System.currentTimeMillis());//更新系统时间
        }
    }

}

线程礼让

在这里插入图片描述

礼让不一定能成功,最终决定权在cpu如何调度

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-9:10
 *
 * 测试礼让,礼让不一定能成功,最终决定权在cpu如何调度
 */
public class TestYield {
    
    
    public static void main(String[] args) {
    
    
        MyYile myYile=new MyYile();
        new Thread(myYile,"a").start();
        new Thread(myYile,"b").start();
    }

}

class MyYile implements Runnable{
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"线程执行结束");
    }
}

Join介绍

在这里插入图片描述

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-9:25
 * //join方法,相当于插队
 */
public class TestJoin implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i <=500; i++) {
    
    
            System.out.println("vip会员开始执行" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        TestJoin testJoin = new TestJoin();
        Thread thread=new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 200; i++) {
    
    
            if(i==100)
            thread.join(); //vip会员插队
            System.out.println("main开始执行"+i);
        }
    }
}

线程状态的观测

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-9:37
 */
public class TestThreadState {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread =new Thread(()->{
    
    
            for (int i = 0; i < 5; i++) {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println("///");
        });


        //观察状态
        Thread.State state=thread.getState();
        System.out.println(state);

        //观察线程启动后
        thread.start();
        state=thread.getState();
        System.out.println(state);

        //
        while (state!=Thread.State.TERMINATED){
    
     //只要线程不停止,就一直输出状态
            Thread.sleep(100);
            state=thread.getState(); //更新线程状态
            System.out.println(state);
        }
    }
}

线程优先级

注意:优先级高的不一定先执行,只不过是执行的概率较大

在这里插入图片描述

守护线程

默认是false,代表用户线程(main线程),守护线程(Gc线程)
在这里插入图片描述

在这里插入图片描述

线程同步(重要)

什么是并发?

在这里插入图片描述

什么是线程同步?

处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步。线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面线程使用完毕 , 下一个线程再使用
采用队列+锁的方式解决
在这里插入图片描述

线程不安全举例

  • 不安全的买票(会出现负数)
 package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:03
 * <p>
 * 模拟不安全的买票
 * 线程不安全,票数有负数
 */
public class TestUnsaveBuyTicket {
    
    
    public static void main(String[] args) {
    
    
        buyTicket buy = new buyTicket();
        new Thread(buy, "wzl").start();
        new Thread(buy, "zzz").start();
        new Thread(buy, "www").start();
    }

}

class buyTicket implements Runnable {
    
    

    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
    
    
        try {
    
    
            while (flag) {
    
    
                buy();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    //买票
    private void buy() throws InterruptedException {
    
    
        while (ticketNums <= 0) {
    
    
            flag = false;
            return;
        }

        Thread.sleep(10);
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
    }
}
  • 不安全的取票
package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:16
 * 不安全取钱
 * 两个人同时去银行取钱
 */
public class TestMoney {
    
    
    public static void main(String[] args) {
    
    

        Account account=new Account(100,"结婚金钱");

        Bank you =new Bank(account,50,"你");
        Bank girl =new Bank(account,100,"女朋友");

        you.start();
        girl.start();
    }
}

//账户
class Account {
    
    
    int money; //余额
    String name;//卡名

    public Account(int money, String name) {
    
    
        this.money = money;
        this.name = name;
    }

}

//银行,模拟取款
class Bank extends Thread {
    
    
    Account account; //账户

    int drawmoney; //取的钱
    int nowmoney; //手里的钱

    public Bank(Account account, int drawmoney,String name) {
    
    

        super(name);
        this.account = account;
        this.drawmoney = drawmoney;

    }

    //用户取钱
    @Override
    public void run() {
    
    
        //钱不够
        if (account.money - drawmoney < 0) {
    
    
            System.out.println(Thread.currentThread().getName() + "钱不够,不能取");
            return;
        }
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //卡内余额
        account.money = account.money - drawmoney;
        //手里的钱
        nowmoney = nowmoney + drawmoney;
        System.out.println(account.name + "余额为:" + account.money);
        System.out.println(this.getName() + "手里的钱" + nowmoney);

    }
}
  • 线程不安全的集合

由于ArrayList是线程不安全的,在测试添加10000条数据后,由于不安全会出现添加的数据不够10000条数据,因为会出现同时抢占同一块内存空间,然后会覆盖原先的值,因此不够10000条大小

 package com.Thread;

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

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:52
 */
public class unsafeList {
    
    
    public static void main(String[] args) {
    
    
        List<String> list =new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
    
    
            new Thread(()->{
    
    
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

如何解决线程不安全问题

使用synchronized 关键词,构建synchronized 方法 和synchronized 块 .
在这里插入图片描述
需要修改内容的时候才使用synchronized同步方法,否则会浪费资源

  • 修改后的安全取票
package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:03
 * <p>
 * 模拟不安全的买票
 * 线程不安全,票数有负数
 */
public class TestUnsaveBuyTicket {
    
    
    public static void main(String[] args) {
    
    
        buyTicket buy = new buyTicket();
        new Thread(buy, "wzl").start();
        new Thread(buy, "zzz").start();
        new Thread(buy, "www").start();
    }

}

class buyTicket implements Runnable {
    
    

    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
    
    
        try {
    
    
            while (flag) {
    
    
                buy();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    //买票,变为同步方法
    private  synchronized void buy() throws InterruptedException {
    
    
        while (ticketNums <= 0) {
    
    
            flag = false;
            return;
        }

        Thread.sleep(10);
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
    }
}
  • 修改后的银行取款

注意:这里不能用synchronized直接修饰run()方法,由于变化的量是accout用户的余额,且两个人共享的资源是同一个账户,而不是共享的银行,因此需要使用同步块进行处理。
同步块中的变量应该进行是增、删、改的变量

package com.Thread;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:16
 * 不安全取钱
 * 两个人同时去银行取钱
 */
public class TestMoney {
    
    
    public static void main(String[] args) {
    
    

        Account account=new Account(100,"结婚金钱");

        Bank you =new Bank(account,50,"你");
        Bank girl =new Bank(account,100,"女朋友");

        you.start();
        girl.start();
    }
}

//账户
class Account {
    
    
    int money; //余额
    String name;//卡名

    public Account(int money, String name) {
    
    
        this.money = money;
        this.name = name;
    }

}

//银行,模拟取款
class Bank extends Thread {
    
    
    Account account; //账户

    int drawmoney; //取的钱
    int nowmoney; //手里的钱

    public Bank(Account account, int drawmoney,String name) {
    
    

        super(name);
        this.account = account;
        this.drawmoney = drawmoney;

    }

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

        synchronized (account){
    
    
            //钱不够
            if (account.money - drawmoney < 0) {
    
    
                System.out.println(Thread.currentThread().getName() + "钱不够,不能取");
                return;
            }
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            //卡内余额
            account.money = account.money - drawmoney;
            //手里的钱
            nowmoney = nowmoney + drawmoney;
            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(this.getName() + "手里的钱" + nowmoney);
        }


    }
}
  • 修改后的安全集合
package com.Thread;

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

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-11:52
 */
public class unsafeList {
    
    
    public static void main(String[] args) {
    
    
        List<String> list =new ArrayList<>();
        for (int i = 0; i < 30000; i++) {
    
    
            new Thread(()->{
    
    
                synchronized (list) {
    
    
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

死锁

  • 定义

在这里插入图片描述

  • 举例

小红和小花进行化妆需要口红和镜子,两人互相拥有一个资源

  • 产生死锁的代码
package com.Thread;

import java.util.List;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-15:24
 */
public class TestSiSuo {
    
    
    public static void main(String[] args) {
    
    

        Makeup girl1=new Makeup(0,"小红");
        Makeup girl2=new Makeup(1,"小花");

        girl1.start();
        girl2.start();
    }
}
class Lipstick{
    
    

}
class Mirror{
    
    

}
class Makeup extends Thread{
    
    

    //需要的资源只有一份,用static保证
    static Lipstick lipstick=new Lipstick();
    static  Mirror mirror=new Mirror();

    int choice;//选择
    String girlName;//女孩名字


    Makeup(int choice,String girlName){
    
    
        this.choice=choice;
        this.girlName=girlName;
    }
    @Override
    public void run() {
    
    
        try {
    
    
            makeup();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    //化妆,互相持有对方的资源
    private void makeup() throws InterruptedException {
    
    
        if(choice==0){
    
    
            synchronized (lipstick){
    
    //持有口红的锁
                System.out.println(this.girlName+"获得了口红的锁");
                Thread.sleep(1000);
                synchronized (mirror){
    
    //1秒后想获得镜子
                    System.out.println(this.girlName+"获得了镜子的锁");
                }
            }

        }else {
    
    
            synchronized (mirror){
    
    //持有镜子的锁
                System.out.println(this.girlName+"获得了镜子的锁");
                Thread.sleep(1000);
                synchronized (lipstick){
    
    //1秒后想获得口红
                    System.out.println(this.girlName+"获得了口红的锁");
                }
            }

        }
    }
}
  • 解决死锁的代码
package com.Thread;

import java.util.List;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-15:24
 */
public class TestSiSuo {
    
    
    public static void main(String[] args) {
    
    

        Makeup girl1=new Makeup(0,"小红");
        Makeup girl2=new Makeup(1,"小花");

        girl1.start();
        girl2.start();
    }
}
class Lipstick{
    
    

}
class Mirror{
    
    

}
class Makeup extends Thread{
    
    

    //需要的资源只有一份,用static保证
    static Lipstick lipstick=new Lipstick();
    static  Mirror mirror=new Mirror();

    int choice;//选择
    String girlName;//女孩名字


    Makeup(int choice,String girlName){
    
    
        this.choice=choice;
        this.girlName=girlName;
    }
    @Override
    public void run() {
    
    
        try {
    
    
            makeup();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    //化妆,互相持有对方的资源
    private void makeup() throws InterruptedException {
    
    
        if(choice==0){
    
    
            synchronized (lipstick){
    
    //持有口红的锁
                System.out.println(this.girlName+"获得了口红的锁");
                Thread.sleep(1000);
            }
            synchronized (mirror){
    
    //1秒后想获得镜子
                System.out.println(this.girlName+"获得了镜子的锁");
            }
        }else {
    
    
            synchronized (mirror){
    
    //持有镜子的锁
                System.out.println(this.girlName+"获得了镜子的锁");
                Thread.sleep(1000);
            }
            synchronized (lipstick){
    
    //1秒后想获得口红
                System.out.println(this.girlName+"获得了口红的锁");
            }
        }
    }
}

死锁避免的方法

在这里插入图片描述

Lock锁

可重入锁

在这里插入图片描述

  • 使用形式
    在这里插入图片描述
  • 举例代码
package com.Thread;

import java.beans.beancontext.BeanContextServiceRevokedListener;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-15:49
 */
public class TestLock {
    
    
    public static void main(String[] args) {
    
    
        Ticket ticket =new Ticket();

        new Thread(ticket).start();
        new Thread(ticket).start();
        new Thread(ticket).start();
    }
}

class Ticket implements Runnable{
    
    

    int ticketNum=10;
    //定义lock锁
    private final ReentrantLock lock =new ReentrantLock();
    @Override
    public void run() {
    
    
        while (true){
    
    
            try {
    
    
                lock.lock(); //加锁
                if(ticketNum>0) {
    
    
                    System.out.println(ticketNum--);
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }else {
    
    
                    break;
                }
            }finally {
    
    
                lock.unlock(); //解锁
            }



        }
    }
}

synchronized vs lock

在这里插入图片描述

package com.Thread;

import jdk.internal.dynalink.ChainedCallSite;

/**
 * @Description
 * @autor wzl
 * @date 2022/8/16-16:07
 * <p>
 * 测试:生产者消费者模型--》利用缓冲区解决:管程法
 * 生产者,消费者,产品,缓冲区
 */
public class TestPc {
    
    
    public static void main(String[] args) {
    
    
        SynContainer synContainer= new SynContainer();
        new productor(synContainer).start();
        new consumer(synContainer).start();
    }
}

class productor extends Thread {
    
    

    SynContainer synContainer;
    public productor(SynContainer synContainer){
    
    
        this.synContainer=synContainer;
    }

    //生产
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            synContainer.push(new chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}

class consumer extends Thread{
    
    
    SynContainer synContainer;
    public consumer(SynContainer synContainer){
    
    
        this.synContainer=synContainer;

    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("消费了-->"+synContainer.pop().id+"只鸡");
        }
    }
}

class chicken {
    
    
    int id; //产品标号

    public chicken(int id) {
    
    
        this.id = id;
    }
}

class SynContainer {
    
    

    //定义一个容器,存放产品
    chicken[] chickens = new chicken[50];
    //定义计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(chicken chicken) {
    
    
        //如果容器满了,就等待消费者消费
        if(count==chickens.length){
    
    
            //通知消费者,生产者等待
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        //如果没有满,就放入产品
        chickens[count]=chicken;
        count++;

        //可以通知消费者消费了
        this.notify();

    }

    //消费者消费产品
    public synchronized chicken pop(){
    
    
        //判断能否消费
        if(count==0){
    
    
            //通知生产者生产,消费者等待
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        //如果有产品,进行消费
        count--;
        chicken chicken=chickens[count];

        //吃完后,通知生产者生产
        this.notify();
        return chicken;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_38716929/article/details/126332241