JAVA系列---synchronized的用法

1. 背景

1.1 多线程操作同一个对象

1.1.1 不加锁

public class HandleThread {
    public static void main(String[] args) {
        SyncThread s1 = new SyncThread();
        SyncThread s2 = new SyncThread();
        Thread t1 = new Thread(s1);
//        Thread t2 = new Thread(s2);
        Thread t2 = new Thread(s1);
        t1.start();
        t2.start();
    }
}



class SyncThread implements Runnable {
//    private int count;
     private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
//        synchronized(this) {
            for (int i = 0; i < 100; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//        }
    }

    public int getCount() {
        return count;
    }
}
  1. 类变量:峰值为170左右;
    在这里插入图片描述

  2. 非类变量:峰值为150左右,
    在这里插入图片描述

类变量的意思就是变量的归属为类,在不加锁的情况下,一定会拿不到期望的变量,但是读取频率比非类变量高。无论是哪种变量,只要不加锁都会发生数据错误。

1.1.2. 加锁

public class HandleThread {
    public static void main(String[] args) {
        SyncThread s1 = new SyncThread();
        SyncThread s2 = new SyncThread();
        Thread t1 = new Thread(s1);
//        Thread t2 = new Thread(s2);

        Thread t2 = new Thread(s1);
        t1.start();
        t2.start();
    }
}



class SyncThread implements Runnable {
    private int count;
//    private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
        synchronized(this) {
            for (int i = 0; i < 100; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}
  1. 类变量: 分隔明显,线程顺序进行,数据不会乱
    在这里插入图片描述

  2. 非类变量:分隔明显,线程顺序进行,数据不会乱,在锁的干预下,多线程变成了单线程,其实类变量和非类变量没什么区别
    在这里插入图片描述
    多线程操作同一个对象,有锁就是线程顺序进行,无锁就是争夺进行。只要不加锁数据就会乱,与是否是类变量无关。

1.2. 多线程操作多个对象

1.2.1. 不加锁

public class HandleThread {
    public static void main(String[] args) {
        SyncThread s1 = new SyncThread();
        SyncThread s2 = new SyncThread();
        Thread t1 = new Thread(s1);
        Thread t2 = new Thread(s2);

//        Thread t2 = new Thread(s1);
        t1.start();
        t2.start();
    }
}



class SyncThread implements Runnable {
    private int count;
//    private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
//        synchronized(this) {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
//    }

    public int getCount() {
        return count;
    }
}
  1. 类对象:争度资源,数据巨大
    在这里插入图片描述
  2. 非类对象: 本质就是单线程
    在这里插入图片描述

1.2.2. 加锁

public class HandleThread {
    public static void main(String[] args) {
        SyncThread s1 = new SyncThread();
        SyncThread s2 = new SyncThread();
        Thread t1 = new Thread(s1);
        Thread t2 = new Thread(s2);

//        Thread t2 = new Thread(s1);
        t1.start();
        t2.start();
    }
}



class SyncThread implements Runnable {
//    private int count;
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
        synchronized(this) {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}
  1. 类对象:争夺资源,因为类对象的原因,数据变大
    在这里插入图片描述

  2. 非类对象:本质是单线程,只是争度资源,数据不乱
    在这里插入图片描述

1.3. 总结

当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象

为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。

2. 修饰方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized

public synchronized void method()
{
   // todo
}

synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:

在子类方法中加上synchronized关键字
class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}


在子类方法中调用父类的同步方法
class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }
} 

注意:

  • 在定义接口方法时不能使用synchronized关键字。
  • 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

3. 修饰代码块

一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞

注意下面两个程序的区别

class SyncThread implements Runnable {
       private static int count;
 
       public SyncThread() {
          count = 0;
       }
 
       public  void run() {
          synchronized(this) {
             for (int i = 0; i < 5; i++) {
                try {
                   System.out.println(Thread.currentThread().getName() + ":" + (count++));
                   Thread.sleep(100);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
             }
          }
       }
 
       public int getCount() {
          return count;
       }
}
 
public class Demo00 {
    public static void main(String args[]){
    //test01
    //SyncThread s1 = new SyncThread();
    //SyncThread s2 = new SyncThread();
    //Thread t1 = new Thread(s1);
    //Thread t2 = new Thread(s2);
    //test02        
        SyncThread s = new SyncThread();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        
        t1.start();
        t2.start();
    }
}

test01是两个线程,两个对象,两个lock,因此会同时执行
test02是两个线程,一个对象,一个lock,因此只能顺序执行

在这里插入图片描述 在这里插入图片描述
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象

为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。

class Counter implements Runnable{
   private int count;
 
   public Counter() {
      count = 0;
   }
 
   public void countAdd() {
      synchronized(this) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
   public void printCount() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + " count:" + count);
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public void run() {
      String threadName = Thread.currentThread().getName();
      if (threadName.equals("A")) {
         countAdd();
      } else if (threadName.equals("B")) {
         printCount();
      }
   }
}
 
public class Demo00{
    public static void main(String args[]){
        Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述
可以看见B线程的调用是非synchronized,并不影响A线程对synchronized部分的调用。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

指定要给某个对象加锁

/**
 * 银行账户类
 */
class Account {
   String name;
   float amount;
 
   public Account(String name, float amount) {
      this.name = name;
      this.amount = amount;
   }
   //存钱
   public  void deposit(float amt) {
      amount += amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   //取钱
   public  void withdraw(float amt) {
      amount -= amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
 
   public float getBalance() {
      return amount;
   }
}
 
/**
 * 账户操作类
 */
class AccountOperator implements Runnable{
   private Account account;
   public AccountOperator(Account account) {
      this.account = account;
   }
 
   public void run() {
      synchronized (account) {
         account.deposit(500);
         account.withdraw(500);
         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
      }
   }
}
 
public class Demo00{
    
    //public static final Object signal = new Object(); // 线程间通信变量
    //将account改为Demo00.signal也能实现线程同步
    public static void main(String args[]){
        Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);
 
        final int THREAD_NUM = 5;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i ++) {
           threads[i] = new Thread(accountOperator, "Thread" + i);
           threads[i].start();
        }
    }
}

在这里插入图片描述
在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。

当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。
public void method3(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj)
   {
      // todo
   }
}



当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance变量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代码块
      }
   }
 
   public void run() {
 
   }
}

本例中去掉注释中的signal可以看到同样的运行结果

4. 修饰方法和修饰代码块的区别

  • 同步方法默认使用this或者当前类做为锁。

  • 同步代码块可以选择以什么来加锁,比同步方法更精确,我们可以选择只有会在同步发生同步问题的代码加锁,而并不是整个方法。

  • 同步方法使用synchronized修饰,而同步代码块使用synchronized(this){}修饰。

线程同步问题大都使用synchronized解决,有同步代码块和同步方法的两种方式,主要记一下这两种的区别

public class SynObj{
 4     public synchronized void showA(){
 5         System.out.println("showA..");
 6         try {
 7             Thread.sleep(3000);
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         }
11     }
12     
13     public void showB(){
14         synchronized (this) {
15             System.out.println("showB..");
16         }
17     }
18     
19     public void showC(){
20         String s="1";
21         synchronized (s) {
22             System.out.println("showC..");
23         }
24     }
25 }



public class Test {
    public static void main(String[] args) {
        final SynObj sy=new SynObj();
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                sy.showA();
            }
        }).start();
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                sy.showB();
            }
        }).start();
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                sy.showC();
            }
        }).start();
    }
}

在这里插入图片描述
代码的打印结果是,showA……showC……会很快打印出来,showB……会隔一段时间才打印出来,那么showB为什么不能像showC那样很快被调用呢?

在启动线程1调用方法A后,接着会让线程1休眠3秒钟,这时会调用方法C,注意到方法C这里用synchronized进行加锁,这里锁的对象是s这个字符串对象。但是方法B则不同,是用当前对象this进行加锁,注意到方法A直接在方法上加synchronized,这个加锁的对象是什么呢?显然,这两个方法用的是一把锁。

由这样的结果,我们就知道这样同步方法是用什么加锁的了,由于线程1在休眠,这时锁还没释放,导致线程2只有在3秒之后才能调用方法B,由此,可知两种加锁机制用的是同一个锁对象,即当前对象。
  另外,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好*。

一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

5. 修饰静态方法

静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Demo00{
    
    public static void main(String args[]){
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。

6. 修饰类

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public static void method() {
      synchronized(SyncThread.class) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}

本例的的给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁

7. 死锁示例

public class Thread01 extends Thread{
     private Object resource01;
     private Object resource02;
     public Thread01(Object resource01, Object resource02) {
     this.resource01 = resource01;
     this.resource02 = resource02;
     }
     @Override
     public void run() {
     synchronized(resource01){
     System.out.println("Thread01 locked resource01");
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     synchronized (resource02) {
     System.out.println("Thread01 locked resource02");
     }
     }
     }
    }
    public class Thread02 extends Thread{
     private Object resource01;
     private Object resource02;
     public Thread02(Object resource01, Object resource02) {
     this.resource01 = resource01;
     this.resource02 = resource02;
     }
     @Override
     public void run() {
     synchronized(resource02){
     System.out.println("Thread02 locked resource02");
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     synchronized (resource01) {
     System.out.println("Thread02 locked resource01");
     }
     }
     }
    }
    public class MainTest {
     public static void main(String[] args) {
     final Object resource01="resource01";
     final Object resource02="resource02";
     Thread01thread01=new Thread01(resource01, resource02);
     Thread02thread02=new Thread02(resource01, resource02);
     thread01.start();
     thread02.start();
     }
    }

8. synchronize心得

在这里插入图片描述
在并发编程中,经常遇到多个线程访问同一个 共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,在java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的,因为对于共享资源属性访问是必要也是必须的

总结

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
  • 虽然说在java中一切皆对象, 但是锁必须是引用类型的,基本数据类型则不可以 。每一个引用类型的对象都可以隐式的扮演一个用于同步的锁的角色,执行线程进入synchronized块之前会自动获得锁,无论是通过正常语句退出还是执行过程中抛出了异常,线程都会在放弃对synchronized块的控制时自动释放锁。
  • 对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。
  • synchronized具有锁重入功能,当一个线程已经持有一个对象锁后,再次请求该对象锁时是可以得到该对象的锁的,这种方式是必须的,否则在一个synchronized方法内部就没有办法调用该对象的另外一个synchronized方法了。锁重入是通过为每个所关联一个计数器和一个占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时计数器会递减,直至减为0时锁才会被释放
  • 在声明一个对象作为锁的时候要注意字符串类型锁对象,因为字符串有一个常量池,如果不同的线程持有的锁是具有相同字符的字符串锁时,两个锁实际上同一个锁。

一句话形容:锁修饰谁就代表同一时间只有一个线程可以操作谁;因此分了很多情况,修饰哪里就决定其特性,修饰类就是类加锁,修饰对象就是对象加锁。

9. 线程等待

提到synchronize就不得不提到线程等待,线程等待的方法有两种,一种是sleep,另一种就是wait,二者最本质的区别就是是否放弃锁。前者不放弃后者放弃

同时也意为着后者必须用在有锁的情况下。

9.1. sleep(随便用)

sleep:当前线程睡觉,既然是睡觉,偏主动行为,因此不放弃锁。
在这里插入图片描述
在这里插入图片描述
总结:只要放到try里面就可以随意使用:Thread.sleep(3000);

9.2. wait(有锁才能用)

wait:当前线程等待,既然是等待,偏被动行为,因此锁被剥夺。

提问

  • wait()方法一定要使用sycronized进行同步吗?不用sycronized修饰会有什么问题?
  • wait()方法会释放对象锁,那么这里指的锁是什么?
  • wait()会释放对象锁,而sleep()不会释放对象锁,这在实际情况中有什么区别?

解答

  • wait()一定要使用sycronized进行同步,否则会报“java.lang.IllegalMonitorStateException”异常。这是因为wait方法会释放对象锁,而此时因为没有用sycronized同步,就没有锁,就会报异常。
  • 锁指的是sycronized修饰的方法、对象、代码块,如下实例中的value。
  • 因为wait()释放了锁,故其他线程可以执行本来由sycronized修饰的内容。例如下面实例中的run()方法内打印value值(System.out.println(value);)。

示例

#如下,我们首先新建自己的线程类,覆写run()方法,新线程的作用是判断传进来的值是不是”123”,如果是就等待10s,不是就修改传进来的值并打印出来。
public class MyThread extends Thread {
    StringBuilder value;
    public MyThread(StringBuilder value) {
        this.value = value;
    }
    @Override
    public void run() {
        try {
            synchronized(value) { 
                System.out.println(value);
                if ((value.toString()).equals("123")) {
                    System.out.println(getName() + "开始等待;当前时间秒数:" + Calendar.getInstance().get(Calendar.SECOND));
                    value.wait(10000);// 注意这里不是说让value进行wait,而是让当前线程进行wait
                } else {
                    value = value.append("2");
                    System.out.println("当前线程名:" + getName() + ";" + value );
                }
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
#然后在主函数中新建三个自己定义的线程并运行:
public static void main(String[] args) {
      // TODO Auto-generated method stub
      StringBuilder value = new StringBuilder("123");
      MyThread myThread1 = new MyThread(value);
      MyThread myThread2 = new MyThread(value);
      MyThread myThread3 = new MyThread(value);
      myThread1.start();
      myThread2.start();
      myThread3.start();
  }


#这个程序的执行结果为:
123
Thread-0开始等待;当前时间秒数:5
123
Thread-2开始等待;当前时间秒数:5
123
Thread-1开始等待;当前时间秒数:5


#如果我们把MyThread类的第9行和第18行,也就是sycronized的修饰去掉,就会报“java.lang.IllegalMonitorStateException”异常。
#如果我们把MyThread类的第13行的value.wait(10000)语句替换为sleep(10000),则输出如下:

123
Thread-1开始等待;当前时间秒数:5
123
Thread-0开始等待;当前时间秒数:15
123
Thread-2开始等待;当前时间秒数:25


由输出的时间秒数可见,使用sleep会使三个线程依此执行,一个等待完10s后另一个才会进入sycronized内进行等待。而使用wait,则三个线程基本都是一起开始等待的,也就是wait会释放对value的锁定,其他线程可以执行sycronized代码块中的语句。另外使用sleep并不一定非要同步,这里是为了验证sleep不会释放锁。

9.3. 总结

只要有线程需要等待,直接使用sleep最好,比较随意,wait使用比较复杂,就像jml15+2里面,使用sleep就是最好的方案,使用wait还需要加锁,本来redis作为分布式锁就比synchronize就更好。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lipviolet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值