java并发编程实战手册第二章笔记

2.2使用synchronized实现 方法,银行存取款的案例,包括线程安全和线程不安全的两种演示代码,包括静态实例方法与静态同步方法的区别,静态同步方法与实例同步方法的使用
2.2.线程安全的操作:

package cn.fans.chapter2.one;

import java.util.concurrent.TimeUnit;

/**
 * 
 * @author fcs
 * @date 2015-4-11
 * 描述:账号类,进行同步的存钱和取钱操作
 * 说明:
 */
public class Acount {
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    /**
     * 
     * 作者:fcs
     * 描述:使用同步synchronized方法
     * 说明:存钱方法  线程安全
     * 返回:
     * 参数:
     * 时间:2015-4-11
     */
    public synchronized  void addCount(int amount){
        int tmp = number;
        try {
            TimeUnit.SECONDS.sleep(2);  //模拟存钱需要两秒的时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp = tmp + amount;
        number = tmp;
    }

    /***
     * 
     * 作者:fcs
     * 描述:使用同步方法,线程安全
     * 说明:
     * 返回:
     * 参数:
     * 时间:2015-4-11
     */
    public synchronized void subCount(int amount){
        int tmp = number;
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tmp = tmp - amount;
        number = tmp;
    }

    /**
     * 线程不安全的存钱操作
     * 描述:
     * 时间:2015-4-11
     */
    public void addCountNoSafe(int amount){
        int tmp = number;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tmp = tmp + amount;
        number = tmp;
    }
    /**
     * 
     * 作者:fcs
     * 描述:线程不安全的取钱操作
     * 说明:当该方法没有加入synchronized修饰的话,而addCountNoSafe
     * 有synchronized修饰的话同样不是线程安全的操作,对于addCountNSafe同理
     * 可以在Main1中运行
     * 
     * 该方法同样说明在类中同步实例方法与非同步实例方法可以同时执行,只是遇到共享变量的情况下
     * 两个不同的实例方法对共享变量的操作是非线程安全的。
     * 返回:
     * 参数:
     * 时间:2015-4-11
     */
    public synchronized void subCountNoSafe(int amount){
        int tmp = number;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp = tmp - amount;
        number = tmp;
    }


}
package cn.fans.chapter2.one;
/**
 * @author fcs
 * @date 2015-4-11
 * 描述:银行类,模拟银行扣款
 * 说明:线程安全的演示
 */
public class Bank implements Runnable{
    private Acount acount;

    public Bank(Acount acount) {
        super();
        this.acount = acount;
    }

    @Override
    public void run() {
        for(int i =0;i< 100;i++){
            acount.subCount(1000);  //每次扣1000,扣100次
        }
    } 

}
package cn.fans.chapter2.one;
/**
 * 
 * @author fcs
 * @date 2015-4-16
 * 描述:线程安全的演示
 * 说明:
 */
public class Company implements  Runnable {
    private Acount acount;

    public Company(Acount acount) {
        this.acount = acount;
    }
    @Override
    public void run() {
        for(int i =0 ;i< 100;i++){
            acount.addCount(1000);
        }
    }
}
package cn.fans.chapter2.one;
/**
 * 
 * @author fcs
 * @date 2015-4-11
 * 描述:存款取款模拟
 * 说明:线程安全的演示
 */
public class Main {
    public static void main(String[] args) {
        Acount acount = new Acount();
        acount.setNumber(1000);
        Bank bank = new Bank(acount);
        Company company = new Company(acount);
        Thread threadBank = new Thread(bank);
        Thread threadCompany = new Thread(company);

        System.out.println("账户初始余额为: "+acount.getNumber());
        threadBank.start();
        threadCompany.start();      
        try {
            threadBank.join();
            threadCompany.join();
            System.out.println("账户最终余额为: "+acount.getNumber());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

2.2.2线程不安全的演示

package cn.fans.chapter2.one;
/**
 * 
 * @author fcs
 * @date 2015-4-11
 * 描述:线程不安全的演示操作
 * 说明:
 */
public class Bank1 implements Runnable{
    private Acount acount;

    public Bank1(Acount acount) {
        super();
        this.acount = acount;
    }

    @Override
    public void run() {
        for(int i =0;i< 100;i++){
            acount.addCountNoSafe(1000);
        }
    }
}
package cn.fans.chapter2.one;
/**
 * 
 * @author fcs
 * @date 2015-4-11
 * 描述:线程不安全的演示操作
 * 说明:
 */
public class Company1  implements Runnable{
    private Acount acount;

    public Company1(Acount acount) {
        super();
        this.acount = acount;
    }

    @Override
    public void run() {
        for(int i =0;i<100;i++){
            acount.subCountNoSafe(1000);
        }
    }
}

2.3使用非依赖属性实现同步

package cn.fans.chapter2.three;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:使用非依赖属性实现同步
 * 说明:售票窗口实体
 */
public class Cinema {
    private long vc1;
    private long vc2;
    private final Object cc1;
    private final Object cc2;
    /**
     * 初始化
     */
    public Cinema(){
        vc1 = 20;
        vc2 = 20;
        cc1 = new Object();
        cc2 = new Object();
    }

    /**
     * 
     * 作者:fcs
     * 描述:窗口一售票
     * 说明:使用synchronized锁住售票窗口一的对象,多线程的情况下只有一个线程能访问该方法
     * 返回:true,false
     * 参数:number
     * 时间:2015-4-12
     */
    public boolean sellTickets1(int number){
        synchronized (cc1) {
            if(number < vc1){
                vc1 = vc1 - number;
                return true;
            }

            return false;
        }
    }

    /**
     * 
     * 作者:fcs
     * 描述:窗口二售票
     * 说明:使用synchronized锁住售票窗口一的对象,多线程的情况下只有一个线程能访问该方法
     * 返回:true,false
     * 参数:number
     * 时间:2015-4-12
     */
    public boolean sellTickets2(int number){
        synchronized (cc2) {
            if(vc2 > number){
                vc2 = vc2 - number;
                return true;
            }
            return false;
        }
    }

    /**
     * 
     * 作者:fcs
     * 描述:窗口一退票
     * 说明:多线程环境下只有一个线程能执行该方法
     * 返回:true,false
     * 参数:
     * 时间:2015-4-12
     */
    public boolean returnTickets1(int number){
        synchronized (cc1) {
            vc1 = vc1 + number;
            return true;
        }
    }

    /**
     * 
     * 作者:fcs
     * 描述:窗口二退票
     * 说明:多线程环境下只有一个线程能执行该方法
     * 返回:true,false
     * 参数:
     * 时间:2015-4-12
     */
    public boolean returnTickets2(int number){
        synchronized (cc2) {
            vc2 = vc2 + number;
            return true;
        }
    }

    /*获取当前售票窗口一的售票数量*/
    public long getVc1(){
        return vc1;
    }

    /*获取当前售票窗口二的数量*/
    public long getVc2(){
        return vc2;
    }
}
package cn.fans.chapter2.three;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:售票窗口实体
 * 说明:
 */
public class TickOffice1 implements Runnable{
    private Cinema  cinema;

    public TickOffice1(Cinema cinema) {
        super();
        this.cinema = cinema;
    }
    @Override
    public void run() {
        cinema.sellTickets1(3);
        cinema.sellTickets1(2);
        cinema.sellTickets2(2);
        cinema.returnTickets1(3);
        cinema.sellTickets1(5);
        cinema.sellTickets2(2);
        cinema.sellTickets2(2);
        cinema.sellTickets2(2);
    }
}
package cn.fans.chapter2.three;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:售票窗口二实体
 * 说明:
 */
public class TickOffice2 implements  Runnable {
    private Cinema  cinema;

    public TickOffice2(Cinema cinema) {
        super();
        this.cinema = cinema;
    }

    @Override
    public void run() {
            cinema.sellTickets2(2);
            cinema.sellTickets2(4);
            cinema.sellTickets1(2);
            cinema.sellTickets1(1);
            cinema.returnTickets2(2);
            cinema.sellTickets1(3);
            cinema.sellTickets2(2);
            cinema.sellTickets1(2);
    }
}
package cn.fans.chapter2.three;

public class Main {
    public static void main(String[] args) {
        Cinema cinema  = new Cinema();
        TickOffice1  tickOffice1 = new TickOffice1(cinema);
        TickOffice2  tickOffice2 = new TickOffice2(cinema);
        Thread tthread1 = new Thread(tickOffice1,"tickOffice1");
        Thread tthread2 = new Thread(tickOffice2,"tickOffice2");

        tthread1.start();
        tthread2.start();

        try {
            tthread1.join();
            tthread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cinema.getVc1());
        System.out.println(cinema.getVc2());
    }
}

2.4在同步代码中使用条件(生产者与消费者问题)
这里演示Object中的多线程方法

package cn.fans.chapter2.four;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:在同步代码中使用条件
 * 说明:生产者消费者队列同步实现
 */
public class EventStorage {
    private int maxsize;
    private List<Date>  storage;
    public EventStorage() {
        maxsize = 10;
        storage = new LinkedList<Date>();
    }
    public synchronized void set(){
        while(storage.size() == maxsize){
            try{
                wait();  //满足条件时让当前线程挂起。
            }catch(InterruptedException e){
                e.printStackTrace();
            }

        }
        storage.add(new Date());
        System.out.printf("Set %d \n",storage.size());
        notifyAll();  //唤醒其他等待的线程
    }
    /**
     * 
     * 作者:fcs
     * 描述:注意这里必须在循环中调用wait()方法,并且不断查询while的条件
     * 直到条件为真的时候才能继续
     * 说明:
     * 返回:
     * 参数:
     * 时间:2015-4-12
     */
    public synchronized void get(){
        while(storage.size() == 0){
            try{
                wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }

        }
        System.out.printf("Get : %d : %s \n",storage.size(),((LinkedList<?>)storage).poll());
        notifyAll();  //注意这里要唤醒其他线程
    }
}
package cn.fans.chapter2.four;

/**
 * 
 * @author fcs
 * @date 2015-4-12 描述:生产者线程 说明:
 */
public class Consumer implements Runnable {
    private EventStorage storage;

    public Consumer(EventStorage storage) {
        super();
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            storage.get();
        }
    }
}
package cn.fans.chapter2.four;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:生产者线程
 * 说明:
 */
public class Producer implements  Runnable{
    private EventStorage  storage;

    public Producer(EventStorage storage) {
        super();
        this.storage = storage;
    }

    @Override
    public void run() {
        for(int i =0 ;i<100000;i++){
            storage.set();
        }
    }
}
package cn.fans.chapter2.four;

public class Main {
    public static void main(String[] args) {
        EventStorage  storage = new EventStorage();
        Producer  producer = new Producer(storage);
        Consumer  consumer = new Consumer(storage);
        Thread pthread = new Thread(producer);
        Thread cthread = new Thread(consumer);
        pthread.start();
        cthread.start();
    }
}

2.5使用锁实现同步(java多线程实现同步的第二种方式,synchronized是第一种方式)
模拟多线程文档打印。

package cn.fans.chapter2.six;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:使用读写锁实现同步访问
 * 说明:使用ReadWriteLock接口。
 */
public class PricesInfo {
    private double price1;
    private double price2;
    private ReadWriteLock lock;
    public PricesInfo(){
        price1 = 1.0;
        price2 = 2.0;
        lock = new ReentrantReadWriteLock();
    }

    /**
     * 
     * 作者:fcs
     * 描述:使用锁机制进行读操作
     * 说明:
     * 返回:price1
     * 参数:
     * 时间:2015-4-12
     */
    public double getPrice1(){
        double value;
        lock.readLock().lock();
        try{
            value = price1;
            System.out.printf("%s: price 1 : %f\n",Thread.currentThread().getName(),price1);

        }finally{
            lock.readLock().unlock();
        }

        return value;
    }

    /**
     * 
     * 作者:fcs
     * 描述:使用锁机制进行读操作
     * 说明:
     * 返回:price2
     * 参数:
     * 时间:2015-4-12
     */
    public double getPrice2(){
        double value;
        lock.readLock().lock();
        try{
            value = price2;
            System.out.printf("%s Price 2 : %f\n ",Thread.currentThread().getName(),price2);
        }finally{
            lock.readLock().unlock();
        }

        return value;
    }

    /**
     * 
     * 作者:fcs
     * 描述:使用锁机制进行写操作
     * 说明:
     * 返回:
     * 参数:price1, price2
     * 时间:2015-4-12
     */
    public void setPrice(double price1,double price2){
        lock.writeLock().lock();
        System.out.printf("Writer: Attempt to modify the prices.\n");

        try{
            this.price1 = price1;
            this.price2 = price2;
        }finally{
            lock.writeLock().unlock();
            System.out.println("Writer:price have been modified.");
        }
    }
}
package cn.fans.chapter2.six;
/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:读线程
 * 说明:
 */
public class Reader implements  Runnable {
    private PricesInfo pricesInfo;

    public Reader(PricesInfo pricesInfo) {
        this.pricesInfo = pricesInfo;
    }

    @Override
    public void run() {
        for(int i =0;i<10;i++){
            pricesInfo.getPrice1();
            pricesInfo.getPrice2();
        }
    }
}
package cn.fans.chapter2.six;

/**
 * 
 * @author fcs
 * @date 2015-4-12
 * 描述:写线程
 * 说明:
 */
public class Writer  implements  Runnable {
    private PricesInfo pricesInfo;

    public Writer(PricesInfo pricesInfo) {
        this.pricesInfo = pricesInfo;
    }

    /**
     * 循环修改两个价格5次,每次修改后线程将休眠2秒钟
     */
    @Override
    public void run() {
        for(int  i=0;i< 5;i++){
            pricesInfo.setPrice(Math.random()*10, Math.random()*10);
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package cn.fans.chapter2.six;

public class Main {
    public static void main(String[] args) {
        PricesInfo  pricesInfo = new PricesInfo();
        Reader readers[] = new Reader[5];
        Thread treader [] = new Thread[5];
        for(int  i =0;i<5;i++){
            readers [i] = new Reader(pricesInfo);
            treader[i] = new Thread(readers[i]);
        }

        Writer writer = new Writer(pricesInfo);
        Thread wthread = new Thread(writer);
        for(int i = 0;i < 5;i++){
            treader[i].start();
        }
        wthread.start();
    }
}

2.7修改锁的公平性(修改2.5代码中构造函数的部分)
演示ReentrantLock和ReentrantReadWriteLock.

package cn.fans.chapter2.seven;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * 
 * @author fcs
 * @date 2015-4-15
 * 描述:修改锁的公平性
 * 说明:
 */
public class PrintQueue {
    private final Lock lock = new ReentrantLock(true);   //参数表示获取锁的策略是非公平的

    public void printJob(Object document){
        lock.lock();   //获取锁
        try {
            long duration = (long)(Math.random() * 10000);
            Thread.sleep(duration / 1000);
            System.out.println(Thread.currentThread().getName()+" Print queue : print a job "+(duration/1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            lock.unlock();   //释放锁
        }

        lock.lock();   //再次获取锁
        try {
            long duration = (long)(Math.random() * 10000);
            Thread.sleep(duration / 1000);
            System.out.println(Thread.currentThread().getName()+" Print queue : print a job "+(duration/1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            lock.unlock();   //释放锁
        }
    }

}
package cn.fans.chapter2.seven;
/**
 * 
 * @author fcs
 * @date 2015-4-16
 * 描述:打印工作
 * 说明:
 */
public class Job  implements Runnable{
    private PrintQueue  printQueue;

    public Job(PrintQueue printQueue) {
        super();
        this.printQueue = printQueue;
    }
    @Override
    public void run() {
        System.out.printf("%s: Goging to print a document \n",Thread.currentThread().getName());
        printQueue.printJob(new Object());
        System.out.printf("%s The document has been printed \n",Thread.currentThread().getName());
    }
}
package cn.fans.chapter2.seven;
/**
 * 
 * @author fcs
 * @date 2015-4-15
 * 描述:修改锁的公平性后,再次获得锁的那个线程将不再是等待时间最长的线程,
 * 说明:
 */
public class Main {
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread [] thread = new Thread[10];
        for(int i =0 ;i< 10;i++){
            thread[i] = new Thread(new Job(printQueue),"Thread"+i);
        }

        for(int i =0;i< 10;i++){
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

第二章 线程同步的基础
1.基本概念
临界区: 用来访问共享资源的代码块,这个代码块在同一个时间里只允许一个线程访问。
临界区应该使用同步机制进行访问,

java提供两种基本的同步机制
1.synchronized关键字机制
2.Lock接口机器是吸纳机制

2.2使用synchronized实现同步
  1.如果一个对象已经使用synchronized关键字声明,那么只有一个执行线程可以访问它。其他线程要访问必须等待。
  2.每一个使用synchronized声明的方法都是临界区
  3.java中同一个对象的临界区,同一时间内只有一个允许备份访问
  4.对于静态同步方法则有不同的行为,一个对象的所有静态同步方法在同一时间内只能有一个线程访问,但是非静态同步方法,可以在该时间内被其他线程访问
  5.如果4中的两种不同的方法在同一时间里由不同的线程修改同一个属性或者值的话,则会出现数据不一致的情况。

  6.在银行存取款的这个实例中,说明一个相关的业务应该是连续的,使用的方法应该进行同步。

  7.synchronized的使用保证了,在并发程序中对共享数据的访问

  8.synchronized会降低程序的性能,因此只能再并发情景中需要修改共享数据的方法上同步使用它。

  9可以递归调用synchronized声明的方法。当一个线程获取一个对象的访问权或者锁的时候,可以访问其中一个同步的方法,或者在其中一个方法中访问另外的同步方法
  而不用额外的同步,这就是多线程的可重入性。

  10.在synchronized代码块中可以使用this表示引用正在执行的方法所属的对象。

  11.synchronized可以修饰方法,修饰属性,可以在代码块上使用,也可以在方法上使用

 2.3 使用非依赖属性实现同步
   1.将synchronized修饰在方法块中,将对象的引用作为传入参数,通常情况下使用this关键字来引用执行方法所述的对象,也可以使用其他的对象对其进行引用。
   2.类中有多个非依赖属性(指的是某些属性的修改对另外一些属性没有影响),被多线程共享,必须进行同步,但是同一时刻只允许一个线程访问一个属性,其他的某个线程访问另一个属性。
   3.synchronized用在代码块上时,使用对象作为传入参数,JVM保证同一时间只有一个线程能够访问这个对象的代码包含块。

 2.4在同步代码块中使用条件------>生产者--消费者模型
   1.这个例子中使用了一个数据缓冲区(Buffer),一个或者多个数据生产者将把数据存入这个缓冲区,一个或者多个数据消费者将数据从缓冲区中取走。
   2.这个缓冲区是个共享数据结构,必须使用同步进行访问控制。
   3.单单使用synchronized关键字不能满足需求,比如当缓冲区满的时候,生产者就不能再放入数据,应该等待,当缓冲区是空的时候,消费者就不能读取数据。
   4.java中的Object中的wait().notify(),notifyAll()方法,这些方法是Object的final native方法,不能被子类重写。

   5.wait()方法: 当一个线程调用该方法的时候,JVM将这个线程置入休眠,并且释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码库。

   6.wait(long millis) wait(long millis,int nanos): 导致线程进入等待状态直到它被通知或者经过指定的时间,这些方法只能在同步方法中使用,如果当前线程不是锁的持有者,该方法抛出一个
   IllegalMonitorStateException异常。

   7.notify(),notifyAll():当 5中休眠的线程需要唤醒的时候,就是用这两个方法,但是有区别的。

   notify()选择唤醒调用5方法的线程是随机的,解除其阻塞状态,如果当前线程不是锁的持有者该方法会抛出一个
   IllegalMonitorStateExcetion异常。

   notifyAll():唤醒全部在同步代码块上调用5方法的线程,解除其阻塞状态。如果当前线程不是锁的持有者该方法会抛出一个
   IllegalMonitorStateExcetion异常。


   8.必须在while循环中调用wait(),并且不断查询while的条件,直到条件为真的时候才能继续。

 2.5使用锁实现同步
   1.锁的同步比synchronized关键字更加强大也更灵活。
   2.Lock接口对临界代码块的控制的获取和释放不会出现在同一个块结构中。
   3.Lock接口允许分离读和写操作,允许多个读线程和只有要个写线程。
   4.Lock接口有更好的性能。

   Lock接口的实现类是ReentrantLock。

   5.在临界区的开始,线程必须使用lock()方法获取对锁的控制,在临界区的结束,必须使用unlock()方法来释放它持有的锁。
   6.如果在临界区最后没有释放锁的话,其他线程可能因为获取不到锁而永久等待,导致死锁。
   7.在临界区中使用try-catch()方式的话,要在finally中释放锁 lock.unlock()

   8.Lock接口还有另外一个方法获取锁,tryLock(),与lock()最大的不同是线程使用tryLock()不能获取锁,tryLock()会立即返回,不会将线程置入休眠。
   tryLock()返回一个布尔值,true表示线程获取了锁,false表示没有获取。而线程调用lock()方法如果不能获取锁的话会休眠,调用tryLock()方法却不会。

   9.如果程序调用了tryLock()方法并且返回true的时候执行了临界区的代码,如果出现错误,则应该引起程序员的重视。

   10.ReentrantLock类也允许使用递归调用,如果一个线程获取了锁并且进行了递归调用,它将继续持有这个锁。
   因此调用Lock()方法以后也将立即返回,并且线程将继续执行递归调用。

   11.注意使用锁可能导致死锁的可能。

  2.6使用读写锁实现同步数据访问
    1.ReadwriteLock接口和其实现类ReentrantReadWriteLock。这个类有两个锁,一个是读操作锁,一个是写操作锁。
    使用读操作锁时可以允许多个线程同时访问,但是使用写操作锁的时候只允许一个线程进行。

    2.读操作锁通过ReadWriteLock接口的readLock()方法获得,写操作锁通过该接口的WriteLock()方法获得。

  2.7修改锁的公平性
    1.ReentrantLock 和ReentrantReadWriteLock 类的构造函数中有一个布尔值fair,表示锁的公平性,如果是false则是公平锁,否则是非公平锁。
    2.默认为公平锁,即公平模式,在该模式下,获取临界区资源的线程往往是等待时间最长的,而在非公平模式下,获取临界区资源的线程是随机选择的。

    3.上面两种模式只适用于lock()和unlock()方法,但是Lock接口的tryLock()并没有将线程休眠,fair属性对该方法的使用没有影响。

    4.读写锁的构造函数也有一个公平策略的参数。该参数的行为与本节中的参数作用一样
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值