Juc并发编程-同步与锁

 

Juc并发编程

 

1、什么是JUC

 

JUC:就是我们Java原生的并发包,和一些常用的工具类!

 

学完之后,很多知识,但是不知道怎么去用!每学习一个知识点,学完之后,可以替换工作中用到的代码!

2、线程基础知识回顾

什么是进程和线程?

进程:QQ.exe

线程:打字、自动保存…..

一个进程可以包含多个线程,一个进程至少有一个线程! Java程序至少有两个线程: GC、Main

并发、并行

并发:多个线程操作同一个资源,交替执行的过程!

并行:多个线程同时执行!只有在多核CPU下才能完成!

所以我们使用多线程或者并发编程的目的:提高效率,让CPU一直工作,达到最高处理性能!

线程有几种状态

线程有 6 种状态,面向源码学习!

public enum State {
    // java能够创建线程吗? 不能!
 	// 新建
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待
    WAITING,
    // 延时等待
    TIMED_WAITING,
    // 终止!
    TERMINATED;
}
wait/Sleep区别

1、类不同!

wait : Obejct 类     Sleep Thread
在juc编程中,线程休眠怎么实现!Thread.Sleep
     // 时间单位
     TimeUnit.SECONDS.sleep(3);

2、会不会释放资源!

sleep:抱着锁睡得,不会释放锁!wait 会释放锁!

3、使用的范围是不同的;

wait 和 notify 是一组,一般在线程通信的时候使用!

sleep 就是一个单独的方法,在那里都可以用!

4、关于异常;

sleep 需要捕获异常!

3、Lock锁

synchronized 传统的方式!

笔记:

代码:

package com.coding.demo01;
// 传统的 Synchronized
// Synchronized 方法 和 Synchronized 块
/*
 * 我们的学习是基于企业级的开发进行的;
 * 1、架构:高内聚,低耦合
 * 2、套路:线程操作资源类,资源类是单独的
 */
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        // 1、新建资源类
        Ticket ticket = new Ticket();
        // 2、线程操纵资源类
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"A").start();
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"B").start();
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"C").start();
    }
}
// 单独的资源类,属性和方法!
// 这样才能实现复用!
class Ticket{
    private int number = 30;
    // 同步锁,厕所 =>close=>
    // synchronized 这是一个关键字
    public synchronized void saleTicket(){
        if (number>0){
            System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
        }
    }
}

问题:

Lock 锁

笔记:

代码:

package com.coding.demo01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
 * JUC之后的操作
 * Lock锁 + lambda表达式!
 */
public class Demo02 {
    public static void main(String[] args) {
        // 1、新建资源类
        Ticket2 ticket = new Ticket2();
        // 2、线程操作资源类 , 所有的函数式接口都可以用 lambda表达式简化!
        // lambda表达式 (参数)->{具体的代码}
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"A").start();
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"B").start();
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"C").start();
    }
}
// 依旧是一个资源类
class Ticket2{
    // 使用Lock,它是一个对象
    // ReentrantLock 可重入锁:回家:大门 (卧室门,厕所门...)
    // ReentrantLock 默认是非公平锁!
    // 非公平锁: 不公平 (插队,后面的线程可以插队)
    // 公平锁: 公平(只能排队,后面的线程无法插队)
    private Lock lock = new ReentrantLock();
    private int number = 30;
    public void saleTicket(){
        lock.lock(); // 加锁
        try {
            // 业务代码
            if (number>0){
                System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }
}
Synchronized 和 Lock 区别

1、Synchronized 是一个关键字、Lock 是一个对象

2、Synchronized 无法尝试获取锁,Lock 可以尝试获取锁,判断;

3、Synchronized 会自动释放锁(a线程执行完毕,b如果异常了,也会释放锁),lock锁是手动释放锁!如果你不释放就会死锁。

4、Synchronized (线程A(获得锁,如果阻塞),线程B(等待,一直等待);)lock,可以尝试获取锁,失败了之后就放弃

image-20200301220037357.pnguploading.4e448015.gif转存失败重新上传取消image-20200301220037357.pnguploading.4e448015.gif转存失败重新上传取消image-20200301220037357.pnguploading.4e448015.gif转存失败重新上传取消

5、Synchronized 一定是非公平的,但是 Lock 锁可以是公平的,通过参数设置;

6、代码量特别大的时候,我们一般使用Lock实现精准控制,Synchronized 适合代码量比较小的同步问题;

4、生产者消费者问题

面试手写题:单例模式、排序算法、死锁、生产者消费者

线程和线程之间本来是不能通信的,但是有时候我们需要线程之间可以协调操作:

Synchronized 普通版
package com.coding.demo01;
// Synchronized 版
/*
目的: 有两个线程:A  B ,还有一个值初始为0,
       实现两个线程交替执行,对该变量 + 1,-1;交替10次
 */
public class Demo03 {
    public static void main(String[] args) {
        Data data = new Data();
        // +1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        // -1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}
// 资源类
// 线程之间的通信: 判断  执行  通知
class Data{
    private int number = 0;
    // +1
    public synchronized void increment() throws InterruptedException {
        if (number!=0){ // 判断是否需要等待
            this.wait();
        }
        number++; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
    // -1
    public synchronized void decrement() throws InterruptedException {
        if (number==0){ // 判断是否需要等待
            this.wait();
        }
        number--; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
}
四条线程可以实现交替吗?不能,会产生虚假唤醒问题!

image-20200301221345082.pnguploading.4e448015.gif转存失败重新上传取消image-20200301221345082.pnguploading.4e448015.gif转存失败重新上传取消image-20200301221345082.pnguploading.4e448015.gif转存失败重新上传取消

注意if 和 while的区别:

package com.coding.demo01;
// Synchronized 版
/*
目的: 有两个线程:A  B ,还有一个值初始为0,
       实现两个线程交替执行,对该变量 + 1,-1;交替10次
       
传统的 wait 和 notify方法不能实现精准唤醒通知!
 */
public class Demo03 {
    public static void main(String[] args) {
        Data data = new Data();
        // +1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
        // -1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}
// 资源类
// 线程之间的通信: 判断  执行  通知
class Data{
    private int number = 0;
    // +1
    public synchronized void increment() throws InterruptedException {
        while (number!=0){ // 判断是否需要等待
            this.wait();
        }
        number++; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
    // -1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){ // 判断是否需要等待
            this.wait();
        }
        number--; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
}
新版的写法 JUC 挂钩!

image-20200301222041342.pnguploading.4e448015.gif转存失败重新上传取消image-20200301222041342.pnguploading.4e448015.gif转存失败重新上传取消image-20200301222041342.pnguploading.4e448015.gif转存失败重新上传取消

image-20200301221901605.pnguploading.4e448015.gif转存失败重新上传取消image-20200301221901605.pnguploading.4e448015.gif转存失败重新上传取消image-20200301221901605.pnguploading.4e448015.gif转存失败重新上传取消

package com.coding.demo01;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
实现线程交替执行!
主要的实现目标:精准的唤醒线程!
    三个线程:A B C
    三个方法:A p5  B p10   C p15 依次循环
 */
public class Demo04 {
    public static void main(String[] args) {
        Data2 data = new Data2();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print10();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
    }
}
// 资源类
class Data2{
    private int number = 1; // 1A 2B  3C
    private Lock lock = new ReentrantLock();
    // 实现精准访问
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    public void print5() throws InterruptedException {
        lock.lock();
        try {
            // 判断
            while (number!=1){
                condition1.await();
            }
            // 执行
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知第二个线程干活!
            number = 2;
            condition2.signal(); // 唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 一定要解锁
        }
    }
    public void print10() throws InterruptedException {
        lock.lock();
        try {
            // 判断
            while (number!=2){
                condition2.await();
            }
            // 执行
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知3干活
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15() throws InterruptedException {
        lock.lock();
        try {
            // 判断
            while (number!=3){
                condition3.await();
            }
            // 执行
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知 1 干活
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

新的技术出来,一定是可以替换一些旧技术的!

5、8锁现象线程彻底理解锁

利用8道题,搞定锁这个概念!

package com.coding.lock8;
import java.util.concurrent.TimeUnit;
/*
1、标准的访问情况下,先执行 sendEmail 还是 sendSMS
   答案:sendEmail
   被 synchronized 修饰的方式,锁的对象是方法的调用者,所以说这里两个方法调用的对象是同一个
   先调用的先执行!
 */
public class LockDemo01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            phone.sendSMS();
        },"B").start();
    }
}
class Phone{
    public synchronized void sendEmail(){
        System.out.println("sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
/*
2、sendEmail休眠3秒后 ,先执行 sendEmail 还是 sendSMS
   答案:sendEmail
   被 synchronized 修饰的方式,锁的对象是方法的调用者,所以说这里两个方法调用的对象是同一个
   先调用的先执行!
 */
public class LockDemo02 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            phone.sendSMS();
        },"B").start();
    }
}
class Phone2{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
/*
3、增加一个普通方法,请问先打印那个 sendEmail 还是 hello
   答案:hello
   新增加的这个方法没有 synchronized 修饰,不是同步方法,不受锁的影响!
 */
public class LockDemo03 {
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone = new Phone3();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
class Phone3{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }
    // 没有 synchronized 没有 static 就是普通方式
    public void hello(){
        System.out.println("hello");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
/*
4、两个手机,请问先执行sendEmail 还是 sendSMS
    答案:sendSMS
    被 synchronized  修饰的方式,锁的对象是调用者;我们这里有两个调用者,两个方法在这里是两个锁
 */
public class LockDemo04 {
    public static void main(String[] args) throws InterruptedException {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone2.sendSMS();
        },"B").start();
    }
}
class Phone4{
    public synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
//LockDemo05.Class 模板,只有一个     static
//new LockDemo05(),可以创建多个对象
/*
5、两个静态同步方法,同一个手机请问先执行sendEmail 还是 sendSMS
    答案:sendEmail
    只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!所以说这里是同一个锁
    并不是因为synchronized
 */
public class LockDemo05 {
    public static void main(String[] args) throws InterruptedException {
        Phone5 phone = new Phone5();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone.sendSMS();
        },"B").start();
    }
}
class Phone5{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public static synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
//LockDemo05.Class 模板,只有一个     static
//new LockDemo05(),可以创建多个对象
/*
6、两个静态同步方法,两个手机,请问先执行sendEmail 还是 sendSMS
    答案:sendEmail
    只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!所以说这里是同一个锁
    并不是因为synchronized
 */
public class LockDemo06 {
    public static void main(String[] args) throws InterruptedException {
        Phone6 phone = new Phone6();
        Phone6 phone2 = new Phone6();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone2.sendSMS();
        },"B").start();
    }
}
class Phone6{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public static synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
//LockDemo05.Class 模板,只有一个     static
//new LockDemo05(),可以创建多个对象
/*
7、一个普通同步方法,一个静态同步方法,只有一个手机,请问先执行sendEmail 还是 sendSMS
    答案:sendSMS
    synchronized 锁的是这个调用的对象
    static 锁的是这个类的Class模板
    这里是两个锁!
 */
public class LockDemo07 {
    public static void main(String[] args) throws InterruptedException {
        Phone7 phone = new Phone7();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone.sendSMS();
        },"B").start();
    }
}
class Phone7{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
package com.coding.lock8;
import java.util.concurrent.TimeUnit;
//LockDemo05.Class 模板,只有一个     static
//new LockDemo05(),可以创建多个对象
/*
7、一个普通同步方法,一个静态同步方法,两个手机,请问先执行sendEmail 还是 sendSMS
    答案:sendSMS
    synchronized 锁的是这个调用的对象
    static 锁的是这个类的Class模板
    这里是两个锁!
 */
public class LockDemo08 {
    public static void main(String[] args) throws InterruptedException {
        Phone8 phone = new Phone8();
        Phone8 phone2 = new Phone8();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //Thread.sleep(200);
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone2.sendSMS();
        },"B").start();
    }
}
class Phone8{
    public static synchronized void sendEmail() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}
小结

1、new this 调用的这个对象,是一个具体的对象!

2、static class 唯一的一个模板!

在我们编写多线程程序得时候,只需要搞明白这个到底锁的是什么就不会出错了!

synchronized(Demo.class){
}
synchronized(this){
}

6、不安全的集合类

只要是并发环境,你的集合类都不安全(List、Map、Set)

list 不安全
package com.coding.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
 * 故障现象:ConcurrentModificationException 并发修改异常
 * 导致原因:add方法没有锁!
 * 解决方案:
 * 1、List<String> list = new Vector<>(); //jdk1.0 就存在的!效率低
 * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
 * 3、List<String> list = new CopyOnWriteArrayList<>();
 * 
 * 什么是 CopyOnWrite; 写入是复制 (思想 COW)
 * 多个调用者同时要相同的资源;这个有一个指针的概念。
 * 读写分离的思想:
 */
public class UnSafeList {
    public static void main(String[] args) {
//        List<String> list = Arrays.asList("a", "b", "c");
//        list.forEach(System.out::println);
//        List<String> list = new ArrayList<>();
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,3));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

COW 的思想:(不要害怕看源码,因为到了最底层都是你能够看懂的代码)

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
set 不安全
package com.coding.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
// ConcurrentModificationException
public class UnSafeSet {
    public static void main(String[] args) {
        // HashSet 底层是什么 就是 HashMap
        // add,就是 HashMap 的 key;
        Set<String> set = new HashSet<>();
//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
//        Set<String> set = new CopyOnWriteArraySet();
        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,3));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}
map 不安全
package com.coding.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
//ConcurrentModificationException
public class UnsafeMap {
    public static void main(String[] args) {
        // new HashMap<>() 工作中是这样用的吗? 不是
        // 加载因子0.75f;,容量 16; 这两个值工作中不一定这样用!
        // 优化性能!
        // HashMap 底层数据结构,链表 + 红黑树
        // = = = = = = =
//        Map<String, String> map = new HashMap<>();
        Map<String, String> map = new ConcurrentHashMap<>();
        // 人生如程序,不是选择就是循环,时常的自我总结十分重要!
        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,3));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值