Java多线程详解,一篇文章彻底搞懂多线程!!!

目录        

1.1. 创建线程的三种方式:

1.2. 多线程的静态代理实现

1.3. Lamda表达式:

1.4. 线程的5大状态

1.5. 线程停止

1.6. 线程休眠 - Sleep()

1.7. 线程礼让 - Yield()

1.8. 线程合并 - Join()

1.9. 线程优先级

1.10. 线程同步方法与同步代码块

1.11. JUC

1.12. 线程死锁

1.13. ReentrantLock-可重入锁

1.14. 线程之间协作(线程通信)

1.15. 线程池

1.15.1. 线程池有那些优势

1.15.2. 常见线程池及创建

1.15.3. 线程结果监控(Future)


       多线程是指同时执行多个线程。这些线程可以并发地运行,并且在同一处理器上交替执行。每个线程都有自己的程序计数器、栈和局部变量,它们独立地执行代码。

注意:线程 start()开启不一定立即执行,由CPU调度执行

1.1. 创建线程的三种方式:

继承Thread类:不建议使用 避免OOP单继承局限性

实现Runnable接口:推荐使用 避免单继承局限性 灵活方便 方便同一个对象被多个线程使用

实现方式: 传入目标对象+Thread对象.start()

实现Callable接口:当执行对象需要执行结果,或需要监测执行过程时使用


/**
 * 多线程代理模拟实现
 **/

public class Race implements Runnable{

    private static String winner = null;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(!isGameOver(i)) {
                //模拟兔子睡觉(20ms)
                if(Thread.currentThread().getName().equals("兔子") && i%50 == 0){
                    try {
                        Thread.sleep(20L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "--> 跑了" + i + "米");
            }
        }
    }

    /**
     * @Description: 判断比赛是否结束
     * @return: boolean
     * @Date: 2023/11/9
     */
    private boolean isGameOver(int steps){
        if(winner != null) {
            return true;
        }else{
            if(steps == 100) {

                winner = Thread.currentThread().getName();

                System.out.println(Thread.currentThread().getName() + " --》是胜利者!");
                return true;
            }else{
                return false;
            }
        }
    }

    public static void main(String[] args) {
        new Thread( new Race(), "乌龟").start();
        new Thread( new Race(), "兔子").start();
    }

}

1.2. 多线程的静态代理实现

/**
 * 静态代理
 **/

public class StaticProxy {

    public static void main(String[] args) {

        //WeddingCompany代理类实现猪八戒结婚
        new WeddingCompany(new ZhuBaJie()).happyMarry();

        //Thread类代理了Runnable接口的run()方法
        new Thread( ()-> System.out.println("猪八戒回高老庄娶媳妇咯!") ).start();
    }
}

/**
 * 事件接口:结婚
 */
interface Marry{

    void happyMarry();

}

/**
 * 主角
 * 参与结婚
 */
class ZhuBaJie implements Marry{

    @Override
    public void happyMarry() {
        System.out.println("猪八戒回高老庄娶媳妇咯!");
    }

}

/**
 * 婚庆公司
 * 作用:代理结婚事件
 */
class WeddingCompany implements Marry{

    //代理的东西-结婚
    private Marry marry;

    WeddingCompany(Marry marry){
        this.marry = marry;
    }

    @Override
    public void happyMarry() {
        before();
        this.marry.happyMarry();
        after();
    }

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

    private void after(){
        System.out.println("结婚后,收尾款");
    }
}

1.3. Lamda表达式:

原理:基于java8 Functional Interface函数式接口实现

函数式接口定义:任何接口如果只包含唯一一个抽象方法,那么他就是一个函数式接口;

对于函数式接口我们可以通过Lamda表达式来创建该接口的对象。

/**
 * Lambda表达式演化过程
 **/

public class LambdaClass {

    public static void main(String[] args) {
        Work work = new Programmer();
        work.doWork();

        // 3、局部内部类
        class DatabaseAdministrator implements Work{
            @Override
            public void doWork() {
                System.out.println("上山能打虎,下山能建表");
            }
        }
        work = new DatabaseAdministrator();
        work.doWork();


        // 4、匿名内部类
        work = new Work() {
            @Override
            public void doWork() {
                System.out.println("匿名职位!");
            }
        };
        work.doWork();

        // 5、lambda表达式:替代创建函数式接口中多余代码
        work = () -> System.out.println("lambda表达式-职位");
        work.doWork();

    }

}

// 1、定义函数式接口
interface Work{

    void doWork();

}

// 2、实现类
class Programmer implements Work{

    @Override
    public void doWork() {
        System.out.println("上山能打虎,下山能编程");
    }
}

1.4. 线程的5大状态

  一、初始状态

  二、运行时状态

  三、阻塞状态

  四、等待状态

  五、超时等待状态

  六、终止状态

1.5. 线程停止

  • 不推荐使用JDK提供的 stop(),destroy()方法。[已废弃]
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量当flag=false,则终止线程运行。

1.6. 线程休眠 - Sleep()

  • sleep(时间)指定当前线程阻塞的毫秒数;
  • sleep存在异常InterruptedException;
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等;
  • 每一个对象都有一个锁,sleep不会释放锁

1.7. 线程礼让 - Yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看CPU心情

1.8. 线程合并 - Join()

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 类似于插队

1.9. 线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调 度哪个线程来执行,优先级越高调度器越先调度;(线程倒置!!
  • 线程的优先级用数字表示,范围从1~10

        Thread.MIN_PRIORITY = 1;

        Thread.NORM_PRIORITY = 5;

        Thread.MAX_PRIORITY = 10;

  • 使用以下方式改变或获取优先级

        Thread.getPriority()

        Thread.setPriority(int xxx)

  • 注意:优先级的设置应该定义在start()方法之前

1.10. 线程同步方法与同步代码块

  • ①线程同步方法 synchronized

synchronized方法控制对“对象”的访,每个对象对应一把锁)每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺陷: 若将一个大的方法申明为synchronized 将会影响效率

  • ②线程同步代码块 synchronized( obj ){ }

        obj : 称之为同步监视器,可以是任何对象,但推荐是共享资源作为监视器

        同步监视器的执行过程

                1、第一个线程访问,锁定同步监视器,执行其中代码     

                2、第二个线程访问,发现同步监视器被锁定,无法访问   

                3、第一个线程访问完毕,解锁同步监视器   

                4、第二个线程访问,发现同步监视器没有锁,然后锁定并访问

/**
  * 线程不安全集合 List
 **/
public class UnsafeList {
	public static void main(String[] args){
    	List<String> list = new ArrayList<String>();
		for (int i = 0;i< 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
			}).start();
        }
        try{
            Thread.sleep( millis: 3000) ;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
	}
}

1.11. JUC

        JAVA JUC是Java的并发包,在Java 5.0提供了java.util.concurrent包,简称为JUC,主要增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括了线程池、异步IO以及轻量级任务框架,提供可调的、灵活的线程池,还提供了设计用于多线程上下文中的Collection实现等。

        JUC的主要作用是更好的支持高并发任务,让开发者进行多线程编程时减少竞争条件和死锁的问题。

线程不安全集合:ArrayList ,LinkedList , HashSet, TreeSet, LinkedHashSet

线程安全集合: Verctor,Hashtable,CopyOnWriteArrayList,ConcurrentHashMap,BlockingQueue,Collections.synchronizedList

1.12. 线程死锁

        多线程死锁是指两个或多个线程在等待对方释放资源,导致程序无法继续执行的情况。

在Java中,多线程死锁通常发生在以下情况

死锁怎么解决:

        资源剥夺 :剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。

        进程回退 :根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。

        进程撤销 :撤销陷入死锁的所有进程,解除死锁,继续运行;逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。

        系统重启 :结束所有进程的执行并重新启动操作系统。

        加锁顺序:当多个线程需要相同的锁时,如果按照不同的顺序加锁,死锁的情况发生率较高,如果按照 相同的顺序加锁,死锁就不会发生。

        使用可重入锁:可重入锁(例如互斥锁)允许一个线程在获得了锁之后再次获取该锁,而不会发生死锁。

        使用超时机制:在获取锁时设置超时时间,如果在超时时间内无法获取锁,则放弃该次获取。

        避免在线程中嵌套使用锁:在线程中嵌套使用锁,可能导致意想不到的死锁情况,应尽量避免这种情况 的发生。

1.13. ReentrantLock-可重入锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制--通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock 类实现了 Lock,它拥有与synchronized(隐式的加锁) 相同的并发性和内存语在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
public class ReentrantLockTest {

    public static void main(String[] args) {

        TicketSell ticketSell = new TicketSell();
        //模拟多个线程同时买票
        new Thread(ticketSell).start();
        new Thread(ticketSell).start();
        new Thread(ticketSell).start();
    }

}

class TicketSell implements  Runnable{

    int ticket = 10;

    //定义自己的可重入锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){

            try {
                //加锁
                lock.lock();

                //执行买票操作
                if(ticket>0) {
                    try {
                        // sleep() 容易模拟出多线程不安全情况
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "买到了第" + ticket-- + "票!");
                }else{
                    break;
                }
            }finally {
                //ReentrantLock最后在finally中释放锁资源
                lock.unlock();
            }

        }
    }
}

1.14. 线程之间协作(线程通信)

①应用场景:生产者和消费者问题

      • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
      • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
      • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待直到仓库中再次放入产品为止

②线程通信

  1. 并发协作模型“生产者/消费者模式” —>管程法
/**
* 管程法 - 监测消费生产模式
**/

public class CachePoolPCTest {

    public static void main(String[] args) {

        SynContainer synContainer = new SynContainer();

        System.out.println("容器大小                生产者                       消费者");

        new Productor(synContainer).start();

        new Comsumer(synContainer).start();

    }

}

/**
* 生产者
*/
class Productor extends Thread{

    SynContainer container;

    Productor(SynContainer container){
        this.container = container;
    }

    /**
* 生产方法
*/
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.putChicken(new Chicken(i));
            System.out.println("【" + container.count + "】" +Thread.currentThread().getName() + "生产了一只Chicken 编号:"  + i );
        }
    }
}

/**
* 消费者
*/
class Comsumer extends Thread{

    SynContainer container;

    Comsumer(SynContainer container){
        this.container = container;
    }

    /**
* 消费方法
*/
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            int id = container.clearChicken().id;
            System.out.println("【" + container.count + "】" + "                                       " + Thread.currentThread().getName() + "消费了一只Chicken 编号" + id );
        }
    }
}

/**
* 产品
*/
class Chicken{

    //产品编号
    int id;

    Chicken(int id){
        this.id = id;
    }
}

//缓冲区
class SynContainer{

    //容器大小
    // CopyOnWriteArrayList<Chicken> chickenList = new CopyOnWriteArrayList<Chicken>();
    Chicken[] chickenArr = new Chicken[10];

    //容器计数器
    int count = 0;

    // private final ReentrantLock lock = new ReentrantLock();

    //生产产品
    public synchronized void putChicken(Chicken chicken){

        //如果容器满了  需要等待消费者消费
        if(chickenArr.length == count) {
            //通知消费者消费,生产者等待
            try {
                this.wait();
                System.out.println("生产-wait(): 容器满了 等待消费者消费");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果产品容器没有满,则装入产品
        chickenArr[count] = chicken;
        count++;

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

    }


    //消费产品
    public synchronized Chicken clearChicken(){

        Chicken chicken = null;

        //如果容器空了需要等待生产者生产
        if(count == 0) {
            try {
                this.wait();
                System.out.println("消费-wait(): 容器空了需要等待生产者生产");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //没有产品时候:通知生产者生产,消费者等待
        count--;
        chicken = chickenArr[count];

        //产品已经被消费了部分 可以继续生产了
        this.notifyAll();

        return chicken;
    }


}
  1. 并发协作模型 “生产者/消费者模式” —>信号灯法 (标志位判断)
/**
* 信号灯法
**/
public class TrafficLightPCTest {

    public static void main(String[] args) {

        Arena arena = new Arena("草船借箭");

        new ZhouYu(arena).start();
        new HuangGai(arena).start();
    }

}

/**
* 周瑜
*/
class ZhouYu extends Thread{

    Arena arena;

    public ZhouYu(Arena arena) {
        this.arena = arena;
    }

/**
* 攻击
*/
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {

            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            arena.attach(arena.name);
            System.out.println(Thread.currentThread().getName() + "开始发出攻击技能 发出第"  + i + "次进攻!");
        }
    }
}

/**
* 黄盖
*/
class HuangGai extends Thread{

    Arena arena;

    public HuangGai(Arena arena) {
        this.arena = arena;
    }

    /**
* 挨打
*/
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {

            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            arena.defend(arena.name);
            System.out.println(Thread.currentThread().getName() + "开始使用护盾技能 抗住第" +  i + "攻击");
        }
    }
}

/**
 * 擂台
**/
class Arena {

    //技能名称
    String name;

    //是否发起了进攻【默认:未进攻】
    boolean flag = false;

    public Arena(String name) {
        this.name = name;
    }

/**
* @Description: 发起进攻
* @param name String
* @return: void
* @Date: 2023/11/12
*/
    public synchronized void attach(String name){

        if(!flag) {
            //已发起进攻 技能【name】, 可以开始防守了!
            this.notifyAll();
            this.flag = true;
            this.name = name;
        }
        
        System.out.println("进攻-->技能名称【" + name + "】!");

    }

    /**
* @Description: 发起防守
* @param name String
* @return: void
* @Date: 2023/11/12
*/
    public synchronized void defend(String name){

        // 没有发起进攻则暂停使用防守技能
        if(!flag) {
            try {
                //已做好防守,通知可以进攻了
                this.wait();
                this.flag = !this.flag;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("防守--》已防住技能【" + name + "】");

    }
}

1.15. 线程池

1.15.1. 线程池有那些优势

降低资源消耗:线程和任务分离,提高线程重用性
控制线程并发数量,降低服务器压力,统一管理所有线程
提高系统响应速度:假如创建线程用的时间为T1,执行任务的时间为T2,销毁线程的时间为T3,那么使用线程池就免去了T1和T3的时间。

1.15.2. 常见线程池及创建

①Java内置线程池:ThreadPoolExecutor

corePoolSize:线程池核心线程数量

maximumPoolSize:线程池最大线程数量

keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit:存活时间的单位

workQueue:存放任务的队列

handler:超出线程范围和队列容量的任务的处理程序

    private final ExecutorService executor = new ThreadPoolExecutor(corePoolSize,
            maximumPoolSize, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(workQueue),
            new CustomizableThreadFactory("YJGZ11-pool-"));

通过Executor工厂类中的静态方法获取线程池对象

      • newCachedThreadPool - 可缓存线程池: new CachedThreadPool(),创建的都是非核心线程,而且最大线程数为Interge的最大值,空闲线程存活时间是1分钟。如果有大量耗时的任务,则不适该创建方式,它只适用于生命周期短的任务(也就是服务器压力比较大)。
      • newFixedThreadPool - 固定数量线程池:固定数量的可复用的线程数,来执行任务。当线程数达到最大核心线程数,则加入队列等待有空闲线程时再执行。
      • newSingleThreadExecutor - 单核心线程线程池:绝对的安全,不考虑性能,因为是单线程,永远只有一个线程来执行任务。

Java内置线程池-子接口:ScheduledExecutorService

当你想控制线程池延迟执行或者重复执行,那么上面创建线程池的三种方式已经不能满足了,这个时候就需要用到我们的线程池的子接口:ScheduledExecutorService

package com.itheima.demo03;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 测试ScheduleExecutorService接口中的延迟执行任务和重复执行任务的功能
 *
 * @author Eric
 * @create 2021-10-02 9:17
 */
public class ScheduleExecutorServiceDemo01 {
    public static void main(String[] args) {
        //1.获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newScheduledThreadPool(3);
        //2.创建多个任务对象,提交任务,每个任务延迟2秒执行
        es.schedule(new MyRunnable(1),2, TimeUnit.SECONDS);
        System.out.println("over");
    }
}

class MyRunnable implements Runnable{
    private int id;

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

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "执行了任务:" + id);
    }
}
package com.itheima.demo03;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * 测试ScheduleExecutorService接口中的延迟执行任务和重复执行任务的功能
 *
 * @author Eric
 * @create 2021-10-02 9:17
 */
public class ScheduleExecutorServiceDemo02 {
    public static void main(String[] args) {
        //1.获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newScheduledThreadPool(3, new ThreadFactory() {
            int n = 1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义线程名称:" + n++);
            }
        });
        //2.创建多个任务对象,提交任务,每个任务延迟2秒执行        //初始等待1秒,执行任务间隔2秒
        es.scheduleAtFixedRate(new MyRunnable2(1),1,2,TimeUnit.SECONDS);
        System.out.println("over");

    }
}

class MyRunnable2 implements Runnable{
    private int id;

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

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + "执行了任务:" + id);
    }
}
package com.itheima.demo03;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * 测试ScheduleExecutorService接口中的延迟执行任务和重复执行任务的功能
 *
 * @author Eric
 * @create 2021-10-02 9:17
 */
public class ScheduleExecutorServiceDemo03 {
    public static void main(String[] args) {
        //1.获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            int n = 1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义线程名称:" + n++);
            }
        });
        //2.创建多个任务对象,提交任务,每个任务延迟2秒执行        //初始延迟1秒,执行任务间隔2秒(任务执行时间不计入的,是等任务执行之后再间隔2秒)
        es.scheduleWithFixedDelay(new MyRunnable3(1),1,2,TimeUnit.SECONDS);
        System.out.println("over");

    }
}

class MyRunnable3 implements Runnable{
    private int id;

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

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + "执行了任务:" + id);
    }
}
1.15.3. 线程结果监控(Future)
package com.itheima.demo04;

import java.util.concurrent.*;

/**
 * 练习异步计算结果
 *
 * @author Eric
 * @create 2021-10-02 10:30
 */
public class FutureDemo {
    public static void main(String[] args) throws Exception {
        //1.获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool();
        //2.创建Callable类型对象
        Future<Integer> f = es.submit(new MyCall(1, 1));
        //3.判断任务是否已经完成
        //test1(f);

        //测试get()方法
        //boolean b = f.cancel(true);
        //System.out.println("取消任务执行的结果:" + b);

        //Integer v = f.get(1, TimeUnit.SECONDS);//由于等待时间过短,任务来不及执行完成,所以会报异常
        //System.out.println("任务执行的结果是:"  +v);
    }

    //正常执行流程
    private static void test1(Future<Integer> f) throws Exception {
        boolean done = f.isDone();
        System.out.println("第一次判断任务是否完成:" + done);

        boolean cancelled = f.isCancelled();
        System.out.println("第一次判断任务是否取消:" + cancelled);

        Integer v = f.get();//一直等待任务的执行,直到完成
        System.out.println("任务执行的结果是:"  + v);

        //再一次判断任务是否完成
        boolean done2 = f.isDone();
        System.out.println("第二次判断任务是否完成:" + done2);

        boolean cancelled2 = f.isCancelled();
        System.out.println("第二次判断任务是否取消:" + cancelled2);
    }
}


class MyCall implements Callable<Integer>{
    private int a;
    private int b;

    //通过构造方法传递两个参数
    public MyCall(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        String name = Thread.currentThread().getName();
        System.out.println(name + "准备开始计算...");
        Thread.sleep(2000);
        System.out.println(name + "计算完成...");
        return a + b;
    }
}

 致谢:【狂神说Java】多线程详解_哔哩哔哩_bilibili

 参考:Java线程池ThreadPoolExecutor详细介绍与使用-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值