线程的基本操作(全)

线程的状态

	public enum State {
	//新生
    NEW,
	//运行
    RUNNABLE,
	//阻塞
    BLOCKED,
	//等待
    WAITING,
	//超时等待
    TIMED_WAITING,
	//终止
    TERMINATED;
}

在这里插入图片描述

NEW状态表示刚刚创建的线程,这种线程还没开始执行。

等到线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切
资源都已经准备好了。

如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。

WAITING和TIMED WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。那么等待的为线程究竟在等什么呢? 一般来说,WAITING的线程正是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。

注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。

新建线程

新建线程很简单。只要使用new关键字创建一个线程对象,并且将它start()起来即可。

Thread t1=new Thread();
t1.start();

那么线程start()后,会干什么呢?这才是问题的关键。线程Thrread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。
这里要注意,下面的代码通过编译,也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

Thread t1=new Thread();
t1.run();

因此,在这里特别注意,调用start()方法和直接调用run()方法的区别。注意:不要用run()方法来开启新线程。它只会在当前线程中串行执行run()方法中的代码。

但考虑到Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runnable接口是一个单方法接口,它只有一个run()方法:

public interface Runnable {
	public abstract void run();
}

此外,Thread类有一个非常重要的构造方法:

public Thread (Runnable target);

它传入一个Runnable接口的实例,在start()方法调用时,新的线程就会执行Runnable.run()方法。实际上,默认的Thread.run()方法就是这么做的。

public void run() {
  if (target != null) {
 		target.run();
  }
} 

默认的Thread.run()方法就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,更为合理。

例子:

public class CreateThread3 implements Runnable {
  	public static void main(String[] args) {
  		Thread t1=new Thread (new CreateThread3());
    	t1.start();
  
  	}    
  
    @Override
    public void run() {
    System.out.println("Oh, I am Runnable"); 
}

除了上面两种常见线程的方式,还有其他方式来创建线程,具体可参考:创建线程的几种方法

终止线程

那么如何正常地关闭一个线程呢?查阅JDK,你不难发现线程Thrread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。但如果你使用Eclipse之类的IDE写代码,就会发现stop()方法是一个被标注为废弃的方法。也就是说,在将来,JDK可能就会移除该方法。

为什么stop()方法被废弃而不推荐使用呢?原因是stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

Thread.stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章地读到了这个不一致的对象,悲剧也就此发生。
在这里插入图片描述

如果需要停止一个线程,那么应该怎么做呢?其实方法很简单,只需要由我们自行决定线程何时退出就可以了。大体逻辑就是在写线程中,处理写逻辑前,增加一个标示位的判断逻辑,可看例子:

public static class ChangeObjectThread extends Thread{
  	volatile boolean stopme = false;
  
  	public void stopMe(){
			stopme = true;
    }
		@override
    public void run() {
    	while (true) {
    		if (stopme){
    			System.out.println("exit by stop me");
    			break;
        }
    		synchronized (u) {
            int v = (int) (System.currentTimeMillis() / 1000);
            u.setId(v);
            //Oh, do sth. else
            try {
            	Thread.sleep (100);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
            u.setName (String.valueOf(v));
        }
        Thread.yield(); 
      }
    }
}

第2行代码定义了一个标记变量stopme,用于指示线程是否需要退出。当stopMe()方法被调用,stopme就被设置为true,此时,在第10行代码检测到这个改动时,线程就自动退出了。使用这种方式退出线程,不会使对象u的状态出现错误。因为,ChangeObjectThread已经没有机会"写坏"对象了,它总是会选择在一个合适的时间终止线程。

线程中断

在Java中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。在上一节中,我们已经详细讨论了stop()方法停止线程的坏处,并且使用了一套自有的机制完善线程退出的功能。

在JDK中是否有提供更强大的支持呢?答案是肯定的,那就是线程中断。

严格地讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到stop()方法的老问题。

有三个方法与线程中断有关,这三个方法看起来很像,可能老会引起混淆和误用。

//中断线程
public void Thread.interrupt()
//判断是否被中断
public boolean Thread.isInterrupted()
//判断是否被中断,并清除当前中断状态
public static boolean Thread.interrupted()

Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。

Thread.isInterrupted()方法也是实例方法,它判断当前线程是否被中断(通过检查中断标志位)。一般情况下,此方法和Thread.interrupt()方法一起使用,Thread.interrupt()中断线程后,我们还需要中断处理的逻辑

最后的静态方法Thrread.interrupted(),也可用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。

例子:

public static void main(String[] args) throws InterruptedException{
  Thread t1 = new Thread(){
    @override
  	public void run(){
      while (true){
        if(Thread.currentThread().isInterrupted()) {
          System.out.println ("Interruted!");
					break;
        }
        Thread.yield();
      }
    }
  };

  t1.start();
  Thread.sleep (2000);
  t1.interrupt();
}

上述代码使用Thread.isInterrupted()函数判断当前线程是否被中断了,如果是,则退出循环体,结束线程。这看起来与前面增加stopme标记的手法非常相似,但是中断的功能更为强劲。如果在循环体中,出现了类似于wait()方法或者sleep()方法这样的操作,则只能通过中断来识别了。

Thread.sleep()函数

下面,先来了解一下Thread.sleep()函数,它的签名如下:

public static native void sleep(long millis) throwws InterruptedException

Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。

public static void main(String[] args) throws InterruptedException{
  Thread t1 = new Thread(){
    @override
  	public void run(){
      while (true){
        if(Thread.currentThread().isInterrupted()) {
          System.out.println ("Interruted!");
					break;
        }
        try {
        	Thread.sleep (2000);
        } catch (InterruptedException e) {
        	System.out.println("Interruted When Sleep");
        	//设置中断状态
        	Thread.currentThread().interrupt();
        }
        Thread.yield();
      }
    }
  };

  t1.start();
  Thread.sleep (2000);
  t1.interrupt();
}

注意上面代码中第10~15行,如果线程在第11行代码处被中断,则程序会抛出异常,并进入第13行代码处理。在catch子句部分,由于已经捕获了中断,我们可以立即退出线程。但在这里,我们并没有这么做,因为也许在这段代码中,我们还必须进行后续的处理来保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法再次中断自己,置上中断标记位。只有这么做,在第6行代码的中断检查中,才能发现当前线程已经被中断了。

注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中再次设置中断标记位。

等待(wait)和通知(notify)

为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程:等待wait()方法和通知notify()方法。

**这两个方法并不是在Thread类中的,而是输出Object类。**这也意味着任何对象都可以调用这两个方法。这两个方法的签名如下:

public final void wait() throws InterruptedException
public final native void notify()

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。这是什么意思呢?

比如,在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,转为等待状态。等待到何时结束呢?线程A会一直等到其他线程调用了obj.notify()方法为止。这时,object对象俨然成了多个线程之间的有效通信手段。

那么wait()方法和notify()方法究竟是如何工作的呢?看下图

在这里插入图片描述

如果一个线程调用了object.wait()方法,那么它就会进入object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object.notify()方法被调用时, 它就会从这个等待队列中随机选择一个线程,并将其唤醒。这里注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。

除notify()方法外,Object对象还有一个类似的notifyAll()方法,它不notify()方法的功能基本一致,不同的是,它会唤醒在这个等待队列中所有等待的我程,而不是随机选择一个。

下面来看下wait()方法与notify()方法的工作细节。

这里还需要强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的synchronzied语句中,无论是wait()方法或者notify()方法都需要首先获得目标对象的一个监视器 。下图显示了wait()方法和notify()方法的工作流程细节。其中T1和T2表示两个线程。T1在正确执行wait()方法前,必须获得object对象的监视器。而wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。

线程T2在notify()方法调用前,也必须获得object对象的监视器。所幸,此时T1已经释放了这个监视器。因此,T2可以顺利获得object对象的监视器。接着,T2执行了notify()方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。(这里的监视器可以理解为锁)
在这里插入图片描述

代码案例:

package com.wlw.test;

public class testThread {
    // 临界区资源
    public static final Object OBJECT = new Object();

    public static class T1 extends Thread {

        @Override
        public void run() {
            // 锁(监视器)
            synchronized (OBJECT) {
                System.out.println("T1 start.");
                try {
                    System.out.println("T1 wait for object");
                    // 等待
                    OBJECT.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("T1 end.");
            }
        }
    }

    public static class T2 extends Thread {
        @Override
        public void run() {
            // 锁(监视器)
            synchronized (OBJECT) {
                System.out.println("T2 start.");
                // 唤醒
                OBJECT.notify();
                System.out.println("T2 notify for object");
                try {
                    // 模拟代码执行,休眠2s
                    Thread.sleep(2000);
                    System.out.println("T2 sleep 2s.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("T2 end.");
            }
        }
    }

    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        t1.start();
        t2.start();
    }
}

上述代码中,开启了TI和T2两个线程。T1执行了object.wait()方法。注意,T1线程执行wait()方法前,T1先申请object的对象锁。因此,在执行object.wait()时,它是持有object的对象锁的。wait()方法执行后,T1会进行等待,并释放object的对象锁。T2在执行 notify()方法之前也会先获得object的对象锁。这里为了让验效果明显,特意安排
在 notify()方法执行之后,让T2休眠2秒,这样做可以更明显地说明,T1在得到notify()方法通知后,还是会先尝试重新获得object的对象锁。上述代码的执行结果类似如下

T1 start.
T1 wait for object
T2 start.
T2 notify for object
T2 sleep 1s.
T2 end.
T1 end.

注意程序打印的时间戳信息,在T2通知T1继续执行后,T1并不能立即继续执行,而是要等待T2释放object的锁,并重新成功获得锁后,才能继续执行。因此,第四行与第五行日志的间隔为2秒(因为T2休眠了2秒)。

注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待苦干时间。除wait()方法可以被唤醒外,另外一个主要区别就是wait()方法会释放目相际对象的锁,而Thread.sleep()方法不会释放任何资源。具体可参考:wait()和sleep()的区别

挂起(suspend)和继续执行(resume)线程

在JDK中Thread类中还会发现两个看起来非常有用的接口,即线程挂起(suspend)和继续执行(resume)。这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续指定。乍看之下,这对操作就像Thread.stop()方法一样好用。但如果你仔细阅读文档说明,会发现它们也早已被标注为废弃方法,并不推荐使用。

不推荐使用suspend()方法去挂起线程是因为suspend()方法在导致线程暂停的同时,并不会释放任何锁资源 。此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume()方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()方法操作意外地在suspend()方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是是Runnable,这也会严重影响我们对系统当前状态的判断。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Nr2TelG-1683624614329)(/Users/wangluwei/Library/Application Support/typora-user-images/image-20230509142607681.png)]

演示下这种情况:

开启t1和t2两个线程。它们会在第12行通过对象锁u实现对临界区的访问。线程tl和t2启动后,在主函数第23~24行中对其进行resume()方法操作,目的是让它们得以继续执行。接着,主函数等待着两个线程的结束。

public class BadSuspend {
	public static Object u = new Object();
	static ChangeObjectThread t1 = new ChangeObjectThread("t1");
	static ChangeObjectThread t2 = new ChangeObjectThread("t2");

	public static class ChangeObjectThread extends Thread {
		public ChangeObjectThread(String name){
			super.setName(name);
		}
		@Override
		public void run() {
			synchronized (u) {
				System.out.println("in "+getName());
				Thread.currentThread().suspend();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		t1.start();
		Thread.sleep(100);
		t2.start();
		t1.resume();
		t2.resume();
		t1.join();
		t2.join();
	}
}

执行上述代码后,我们可能会得到以下输出:

in t1
in t2

这表明两个线程先后进入了临界区,但是程序不会退出,而是会挂起。使用jstack命令打印系统的线程信息可以看到:

"t2" #9 prio=5 os_prio=0 tid=0x15c85c00 nid=0x1dddc runnable [0x15f2f000]
java.lang.Thread. State: RUNNABLE
at java.lang.Thread.suspend0 (Native Method)
at java.lang.Thread. suspend (Thread.java:1029)
at geym.conc.ch2.suspend.BadSuspend$ChangeObjectThread.run(BadSuspend. java:16)
- locked <0x048b2e58> (a java.lang.Object)

这时我们需要注意,在当前系统中,线程t2其实是被挂起的的,但是它的线程状态确实是RUNNABLE,这很有可能使我们误判当前系统的状态。同时,虽然主函数中已经调用了resume()方法,但是由于时间先后顺序的缘故,那个resume并没有生效!这就导致了线程t2被永远挂起,并且永远占用了对象u的锁。这对于系统来说极有可能是致命的。

如果需要一个比较可靠的suspend()方法,那么应该怎么办呢?回想一下上一节中提到的wait()方法和notify()方法,这也不是一件难事。下面的代码就给出了一个利用wait()方法和notify()方法,在应用层面实现suspend()方法和resume()方法功能的例子。看完之后发现我们完全可以利用Object类中的wait()方法和notify()方法来实现线程停止和继续执行的功能,这也难怪为什么要废弃uspend()方法和resume()方法

public class GoodSuspend {
    public static Object u = new Object();

    public static class ChangeObjectThread extends Thread {
        volatile boolean suspendme = false;
        
        public void suspendMe() {
            suspendme = true;
        }

        public void resumeMe(){
            suspendme=false;
            synchronized (this){
                notify();
            }
        }
        @Override
        public void run() {
            while (true) {

                synchronized (this) {
                    while (suspendme)
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }

                synchronized (u) {
                    System.out.println("in ChangeObjectThread");
                }
                Thread.yield();
            }
        }
    }

    public static class ReadObjectThread extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (u) {
                    System.out.println("in ReadObjectThread");
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ChangeObjectThread t1 = new ChangeObjectThread();
        ReadObjectThread t2 = new ReadObjectThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.suspendMe();
        System.out.println("suspend t1 2 sec");
        Thread.sleep(2000);
        System.out.println("resume t1");
        t1.resumeMe();
    }
}

在代码第5行,用标记变量suspendme表示当前线程是否被挂起。同时,增加了suspendMe()和resumeMe()两个方法,分别用于挂起线程和继续执行线程。

在代码第2128行中,线程会先检查自己是否被挂起,如果是,则执行wait()方法进行等待。否则,则进行正常的处理。当线程继续执行时,resumeMIe()方法被调用(代码第1116行),线程t1得到一个继续执行的notify()方法通知,并且清除了挂起标记,从而得以正常执行。

等待线程结束(join)

在很多情况下,线程之间的协作和人与人之间的协作非常常类似。一种非常常见的合作方式就是分工合作。以我们非常熟悉的软件开发为例,在一个项目进行时,研发人员需要等待需求分析师完成他的工作,然后才能进行研发。

将这个关系对应到多线程应用中,很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能。如下所示,显示了两个join()方法:

public final void join() throws InterruptedExcepticon
public final synchronized void join(long millis) thnrows InterruptedException

第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标标线程执行完毕。

第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为"等不及了",而继续往下执行。

英文join的翻译,通常是加入的意思。这个意思在这里也非?常贴切。因为一个线程要加入另外一个线程,最好的方法就是等着它一起走。

public class JoinMain {
    public volatile static int i=0;
    public static class AddThread extends Thread{
        @Override
        public void run() {
            for(i=0;i<10000000;i++);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddThread at=new AddThread();
        at.start();
        at.join();
        System.out.println(i);
    }
}

在主函数中,如果不使用join()方法等待AddThread,那么得到到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,故在join()方法返回,AddThread已经执行完成,因此i总是10000 000。

有关join()方法,我还想再补充一点,join()方法的本质是让调用线程wait()方法在当前线程对象实例上。

下面是JDK中join()方法实现的核心代码片段:

package java.lang;

public class Thread implements Runnable {
    
  	public final void join() throws InterruptedException {
        join(0);
    }
  
		public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
}

while (isAlive()) {
  wait(0);
}

可以看到,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()方法通知所有的等待线程继续执行。

因此,值得注意的一点是:不要在应用程序中,在Thread对象实例上使用类似wait()方法或者notify()方法等,因
为这很有可能会影响系统API的工作,或者被系统API所影响。

谦让(yeild)

Thread.yield(),它的定义如下:

public static native void yield();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CCPU资源的争夺,但是否能够再次被分配到就不一定了。

因此,对Thread.yield()方法的调用就好像是在说:”我已经完成了一些最重要的工作了,我可以休息一下了,可以给其他他线程一些工作机会啦!“

如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield()方法,给予其他重要线程更多的工作机会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在PyQt6中,可以使用QThread类来实现线程操作线程是一种并发执行的机制,可以在应用程序中同时执行多个任务,从而提高程序的性能和响应能力。 以下是PyQt6中线程操作基本步骤: 1. 创建一个继承自QThread的自定义线程类,并重写其run()方法。在run()方法中编写需要在子线程中执行的代码逻辑。 2. 在主线程中创建自定义线程类的实例。 3. 连接自定义线程类的信号与主线程中的槽函数,以便在子线程执行完毕后,将结果传递给主线程进行处理。 4. 调用自定义线程类的start()方法,启动线程。 下面是一个简单的示例代码,演示了如何在PyQt6中使用线程操作: ```python from PyQt6.QtCore import QThread, pyqtSignal class MyThread(QThread): finished = pyqtSignal(str) def run(self): # 在子线程中执行的代码逻辑 result = "Hello, World!" self.finished.emit(result) # 主线程 def handle_result(result): print(result) thread = MyThread() thread.finished.connect(handle_result) thread.start() ``` 在上面的示例中,我们创建了一个自定义线程类MyThread,重写了其run()方法,在其中执行了一段逻辑,并通过finished信号将结果传递给主线程。主线程中的handle_result函数用于处理子线程执行完毕后的结果。 注意,PyQt6中的线程操作涉及到多线程编程,需要注意线程间的数据共享和同步问题,以避免出现竞态条件等并发问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

悬浮海

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

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

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

打赏作者

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

抵扣说明:

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

余额充值