多线程---多线程基础知识

2. 线程的基本概念

2.1 启动线程的5中方法

  1. new MyThread().start()
  2. new Thread®.start
  3. new Thread(lambda).start()
  4. ThreadPool
  5. Future Callable and FutureTask

代码示例

package com.cyc.juc.c_000_00_threadbasic;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * @description 启动线程的5种方式
 * @author cyc
 * @date 2021-07-25 20:11:02
 */
public class T02_HowToCreateThread {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }

    /**
     * 使用实现Runable方式更灵活, 因为MyRun类还可以继承其他类,而继承Thread方式,则不可再继承其他的类
     */
    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }

    /**
     * 实现Callable<T>接口可返回数据, 可以指定返回泛型
     */
    static class MyCall implements Callable<String> {

        @Override
        public String call() throws Exception {
            System.out.println("Hello MyCall!");
            return "success";
        }
    }

    //启动线程的5种方式
    public static void main(String[] args) throws Exception {
        //1. 继承Thread类
        new MyThread().start();

        //2. 实现Runnable接口
        new Thread(new MyRun()).start();

        //3. 使用lambda表示式
        new Thread(() -> {
            System.out.println("Hello Lambda!");
        }).start();

        //4. 实现Callable接口
        FutureTask<String> task = new FutureTask<>(new MyCall());
        Thread thread = new Thread(task);
        thread.start();

        //5. 线程池
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(() -> {
            System.out.println("Hello Thread Pool");
        });

        Future<String> f = service.submit(new MyCall());
        //Future.get()方法是线程阻塞的状态, 什么时候MyCall执行完了, 直到f.get拿到值为止,才能继续往下执行
        String s = f.get();
        System.out.println(s);
        service.shutdown();

        //以上这些创建线程的方式, 本质上是一种,都是new 一个Thread()对象出来, 然后调用他的start方法
    }

}

2.2 线程的状态

2.2.1 JAVA的6种线程状态

  1. NEW : 线程刚刚创建,还没有启动
  2. RUNNABLE : 可运行状态(包含READY和RUNNING两种细分状态)
  3. WAITING : 等待被唤醒
  4. TIMED_WAITING : 隔一段时间后自动唤醒
  5. BLOCKED : 被阻塞,正在等待锁
  6. TERMINATED : 线程结束

如下图

在这里插入图片描述

每种状态的代码演示

1. NEW
        Thread t1 = new Thread(() -> {
            System.out.println("2: " + Thread.currentThread().getState());
            for (int i = 0; i < 3; i++) {
                SleepHelper.sleepSeconds(1);
                System.out.println(i + " ");
            }
        });
        //这里还没有调用start方法, 所以线程是new状态
        System.out.println("1: " + t1.getState());
2. RUNNABLE (RUNNING+READY)
Thread t1 = new Thread(() -> {
    System.out.println("2: " + Thread.currentThread().getState());
    for (int i = 0; i < 3; i++) {
        SleepHelper.sleepSeconds(1);
        System.out.println(i + " ");
    }
});
//这里还没有调用start方法, 所以线程是new状态
System.out.println("1: " + t1.getState());
//这里调用start方法, 线程开始执行,状态是RUNNABLE, 其中RUNNABLE可分为ready和running,
// CPU时间片轮到这个线程了, 它就是running, 没有轮到这个线程, 它就是ready
t1.start();
//
t1.join();
System.out.println("3: " + t1.getState());
3. WAITING
Thread t2 = new Thread(() -> {
    //park用于挂起当前线程,什么时候被叫醒了 , 什么时候执行下面的
    LockSupport.park();
    System.out.println("t2 go on!");
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
t2.start();
//执行LockSupport.park(),这里睡一秒钟, 让当前线程完全执行起来,
TimeUnit.SECONDS.sleep(1);
//由于 LockSupport.park()将当前线程挂起了, 所以这里的线程状态是WAITING
System.out.println("4: " + t2.getState());
4. TIMED_WAITING
Thread t2 = new Thread(() -> {
    //park用于挂起当前线程,什么时候被叫醒了 , 什么时候执行下面的
    LockSupport.park();
    System.out.println("t2 go on!");
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
t2.start();
//执行LockSupport.park(),这里睡一秒钟, 让当前线程完全执行起来,
TimeUnit.SECONDS.sleep(1);
//由于 LockSupport.park()将当前线程挂起了, 所以这里的线程状态是WAITING
System.out.println("4: " + t2.getState());

LockSupport.unpark(t2);
//睡一秒钟, 确认t2已经被叫醒
TimeUnit.SECONDS.sleep(1);
//因为这里醒了之后, 会执行sleep,所以这里的状态是TIMED_WAITING
System.out.println("5: " + t2.getState());
5. BLOCKED

在多线程中, 使用synchronized给对象加锁,在锁未释放,其他线程获取不到时, 此时获取不到锁的线程为

阻塞状态(BLOCKED),因为synchronize经过操作系统的调度,所以,对于线程来说,也只有使用synchronized, 线程才会有BLOCKED状态, 其余锁对线程来说都是WAITING状态

		final Object o = new Object();

        Thread t3 = new Thread(() -> {
            synchronized (o) {
                System.out.println("t3 得到了锁 o");
            }
        });

        SleepHelper.sleepSeconds(1);

        //新启动一个线程, 获取这把锁, 并睡5秒钟
        new Thread(() -> {
            synchronized (o) {
                SleepHelper.sleepSeconds(5);
            }
        }).start();

        //主线程睡一秒, 保证上面的线程已经锁定了这把锁o
        SleepHelper.sleepSeconds(1);

        //t3线程启动,去尝试获取锁,此时获取不到锁
        t3.start();
        // 主线程睡一秒, 保证t3已经启动完成
        SleepHelper.sleepSeconds(1);
        //t3线程去拿这把锁, 拿不到, 所以状态是BLOCKED
        System.out.println("6: " + t3.getState());

和WAITING状态对比如下

 		final Lock lock = new ReentrantLock();

        Thread t4 = new Thread(() -> {
            //Lock属于JUC的锁, 使用CAS实现, CAS实现的锁, 会进入一种忙等待, 不会进入BLOCKED状态, 进入的是WAITING状态
            lock.lock();//这里省略try catch
            System.out.println("t4 获取到了锁");
            lock.unlock();
        });

        new Thread(() -> {
            lock.lock();
            //这里睡5秒, 也就是当前线程持有这把锁5秒
            SleepHelper.sleepSeconds(5);
            lock.unlock();
        }).start();

        //主线程睡一秒, 确保上面的线程完全启动
        SleepHelper.sleepSeconds(1);

        t4.start();
        //主线程睡一秒, 确保t4启动完毕
        SleepHelper.sleepSeconds(1);
        //由于上面的线程持有锁5秒, 减去上面睡的共计2秒, 释放锁时间还剩3秒,
        // 所以t4目前还是没有获取到锁, 此时的状态为WAITING
        System.out.println("7: " + t4.getState());
        Thread t5 = new Thread(() -> {
            LockSupport.park();
        });

        t5.start();

        SleepHelper.sleepSeconds(1);
        //除synchronized之外, 其余的锁对线程来说都是WAITING状态
        System.out.println("8: "+t5.getState());
6. TERMINATED
Thread t1 = new Thread(() -> {
    System.out.println("2: " + Thread.currentThread().getState());
    for (int i = 0; i < 3; i++) {
        SleepHelper.sleepSeconds(1);
        System.out.println(i + " ");
    }
});
//这里还没有调用start方法, 所以线程是new状态
System.out.println("1: " + t1.getState());
//这里调用start方法, 线程开始执行,状态是RUNNABLE, 其中RUNNABLE可分为ready和running,
// CPU时间片轮到这个线程了, 它就是running, 没有轮到这个线程, 它就是ready
t1.start();
//
t1.join();
//线程执行结束
System.out.println("3: " + t1.getState());

2.2 线程的打断

2.2.1 interrupt的三个方法

//Thread.java
public void interrupt()  // t.interrupt() 打断t线程(设置t线程的标志位f=true,并不是打断线程的运行)
public boolean isInterrupt() //t.isinterrupt() 查询打断标志位是否被设置(是不是曾经被打断过)
public static boolean interrupted() //Thread.interrupt() 查看"当前"线程是否被打断,如果被打断,则恢复标志位

代码示例

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (; ; ) {
                if (Thread.currentThread().isInterrupted()) {
                    //检查当前线程是否被设置过标志位, 如果设置过, 则打印结果,并跳出循环
                    System.out.println("Thread is interrupt!");
                    System.out.println(Thread.currentThread().isInterrupted());
                    //优雅的结束线程的方式, 设置标志位, 并在程序运行中检查标志位,如果设置了, 则结束该线程, 如下方的break
                    break;
                }
            }
        });
        t.start();
        SleepHelper.sleepSeconds(2);
        //两秒后 ,设置t线程的标志位
        t.interrupt();
    }
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (; ; ) {
                if (Thread.interrupted()) {
                    //interrupted会检查当前线程是否被打断,并重置标志位
                    System.out.println("Thread is interrupt!");
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t.start();
        SleepHelper.sleepSeconds(2);
        //两秒后 ,设置t线程的标志位
        t.interrupt();
    }

2.2.2 interrupt和sleep() wait() join()

sleep()方法在睡眠的时候,不到时间是没有办法唤醒的,这个时候可以用interrupt设置标志位,然后必须的catchInterruptException来进行处理,决定继续睡或者是别的逻辑,(自动进行中中断标志位复位)

public class T07_Interrupt_and_sleep {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                //线程被打断异常, 抛异常后, JAVA默认会将该线程的标志位重置
                System.out.println("Thread is interrupted!");
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });
        t.start();

        SleepHelper.sleepSeconds(5);
        //5秒后, 给线程设置标志位
        //在线程sleep(),wait(),join()操作中, 设置标志位会导致抛InterruptedException异常,
        // 此时需手动捕获异常,并进行处理, 可以继续运行, 也可以结束线程
        t.interrupt();
    }
}
public class T08_Interrupt_and_wait {

    private static Object o = new Object();

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            synchronized (o) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    //线程被打断异常, 抛异常后, JAVA默认会将该线程的标志位重置
                    System.out.println("Thread is interrupted!");
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t.start();

        SleepHelper.sleepSeconds(5);
        //5秒后, 给线程设置标志位
        //在线程sleep(),wait(),join()操作中, 设置标志位会导致抛InterruptedException异常,
        // 此时需手动捕获异常,并进行处理, 可以继续运行, 也可以结束线程
        t.interrupt();
    }
}

2.2.3 interrupt 是否能中断正在竞争锁的线程

synchronized锁
/**
 * @Description: 设标志位能不能把争抢锁的线程打断?
 * 结论: synchronized锁竞争的过程是不会被interrup()打断的
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 11:24
 */
public class T09_Interrupt_and_sync {

    private static Object o = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (o) {
                SleepHelper.sleepSeconds(10);
            }
        });
        //t1 启动后, 锁定这把锁10秒钟
        t1.start();

        SleepHelper.sleepSeconds(1);

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                System.out.println("t2 抢到了这把锁");
            }
            System.out.println("t2 end!");
        });
        //启动t2, t2去抢这个锁, 但是由于t1还在持有这把锁, 在9秒后才会释放, 此时t2处于阻塞(BLOCKED)状态
        t2.start();
        SleepHelper.sleepSeconds(1);
        System.out.println("t2的状态: "+t2.getState());
        //由于这里这是设置标志位, 并不是打断线程, 所以不会打断t2竞争锁的过程
        t2.interrupt();
    }
}

Lock锁
/**
 * @Description: 设标志位能不能把争抢锁的线程打断?
 * 结论: ReentrantLock锁竞争的过程是不会被interrup()打断的
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 11:24
 */
public class T10_Interrupt_and_lock {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println("t1 end");
        });
        //t1 启动后, 锁定这把锁10秒钟
        t1.start();

        SleepHelper.sleepSeconds(1);

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {

            } finally {
                lock.unlock();
            }
            System.out.println("t2 end!");
        });
        //启动t2, t2去抢这个锁, 但是由于t1还在持有这把锁, 在9秒后才会释放, 此时T2处于WAITING状态
        t2.start();
        SleepHelper.sleepSeconds(1);
        System.out.println("t2的状态: " + t2.getState());
        //由于这里这是设置标志位, 并不是打断线程, 所以不会打断t2竞争lock锁的过程
        t2.interrupt();
    }
}

如何打断锁竞争的过程

如果竞争锁过程中,不想被打断 ,使用Lock.lock()加锁, 如果想可以被打断, 则使用Lock.lockInterruptibly()

/**
 * @Description: 设标志位能不能把争抢锁的线程打断?
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 11:50
 */
public class T11_Interrupt_and_lockInterruptibly {

    private static ReentrantLock lock = new ReentrantLock();
 
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println("t1 end");
        });
        //t1 启动后, 锁定这把锁10秒钟
        t1.start();

        SleepHelper.sleepSeconds(1);

        Thread t2 = new Thread(() -> {
            System.out.println("t2 start!");
            try {
                //lockInterruptibly(), 抢锁的过程可被打断
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println("t2 end!");
        });
        //启动t2, t2去抢这个锁
        t2.start();
        SleepHelper.sleepSeconds(1);
        //由于这里这是设置标志位, 并不是打断线程, 所以不会打断t2竞争lock锁的过程
        t2.interrupt();
    }
}

3. 优雅的结束线程

如何优雅的结束一个线程?

例如: 上传一个大文件,正在处理费时的计算,如何优雅的结束这个线程?

结束线程的方法:

  1. 自然结束(能自然结束的就尽量自然结束)
  2. stop() , suspend() , resume()
  3. volatie标志
    1. 不适合某些场景(比如还没有同步的时候,线程做了阻塞操作,没有办法循环回去)
    2. 打断时间也不是特别精确,比如一个阻塞容器,容量为5的时候结束生产者,但是,由于volatile同步线程标志位的时候的时间控制不是特别精确,有可能生产者还继续生产一段时间
  4. interrupt() and isInterrupt(比较优雅)
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                //假设每隔一秒钟上传一个程序
                System.out.println("go on");
                SleepHelper.sleepSeconds(1);
            }
        });

        t.start();

        SleepHelper.sleepSeconds(5);
        //为什么不建议使用stop方法,
        //太粗暴了,他会释放所有锁, 容易造成数据不一致的问题
        t.stop();
    }
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                //假设每隔一秒钟上传一个程序
                System.out.println("go on");
                SleepHelper.sleepSeconds(1);
            }
        });

        t.start();

        SleepHelper.sleepSeconds(5);
        //暂停, 不会释放锁,容易产生死锁问题, 如果没有resume , 那么这把锁就永远不会被释放,
        t.suspend();
        SleepHelper.sleepSeconds(3);
        //恢复执行
        t.resume();
    }
    private static volatile boolean running = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            long i = 0L;
            while (running) {
                //wait recv accept
                i++;
            }
            System.out.println("end and i = " + i);
        });
        t.start();

        SleepHelper.sleepSeconds(1);
        running = false;
    }
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.interrupted()) {
                //sleep wait
            }
            System.out.println("t end");
        });
        t.start();

        SleepHelper.sleepSeconds(1);
        //使用interrupt更加优雅, 但是依然不能精确控制
        t.interrupt();
    }

4. 线程三大特性

可见性

在这里插入图片描述

代码示例

public class T01_HelloVolatile {

    private static boolean running = true;

    private static void m() {
        System.out.println("m start");
        while (running){
//            System.out.println("hello");
        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        new Thread(T01_HelloVolatile::m,"t1").start();

        SleepHelper.sleepSeconds(1);

        running = false;
    }
}

这个程序 , 如果不手动停止, 循环就不会停止

解析:

running首先位于主内存中, 每个线程都会拿主内存中running的值,放到线程本地的缓存中一份,每次循环,并不是去读主内存的running, 而是去读自己缓存中running的值,所以这个值只要没有从新从主线程中取过, 那线程使用的running的值, 就一直是本地缓存中的。此时主线程将自己本地缓存中的running改为了false,但是t1线程中的running值并没有改变, 所以, 这里循环一直不会结束。

  • 添加 volatile修饰
// volatile所修饰的这块内存, 对于这块内存任何的修改, 都对其他线程可见, 即保持可见性
//其他线程每次读这个running的值, 都要去主内存中取, 因此主线程中修改的running值, 会立即刷新到主内存中,
//t1线程重新读取主内存中running的值
private static volatile boolean running = true;
  • 某些语句触发内存缓存同步刷新

    例如:system.out.print()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过观察print源码, 发现里面有synchronized修饰,会在某些情况下, 触发更新running在主内存中的值。

注意: volatile 引用类型(包括数组) 只能保证引用本身的可见性, 不能保证内部字段的可见性

例如

/**
 * @Description: volatile 引用类型(包括数组) 只能保证引用本身的可见性, 不能保证内部字段的可见性
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 15:02
 */
public class T02_VolatileReference {

    private static class A{
        boolean running = true;

        void m(){
            System.out.println("m start");
            while(running){
            }
        }
    }

    private volatile static A a = new A();

    public static void main(String[] args) {
        new Thread(a::m,"t1").start();
        SleepHelper.sleepSeconds(1);
        a.running = false;
    }
}

因为volatile修饰的是对象a,a引用指向了堆内存中的示例,但是a中的字段running修改后并不会被刷新到主内存中,所以这个循环永远不会结束。因此,volatile关键字一般用在字段上而不是对象上。

有序性

先看一个例子

/**
 * @author cyc
 * @date 2021/7/29 16:51
 */
public class T01_Disorder {

    private static int x = 0, y = 0;

    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        for (long i = 0; i < Long.MAX_VALUE; i++) {

            x = 0;
            y = 0;
            a = 0;
            b = 0;

            CountDownLatch latch = new CountDownLatch(2);

            Thread one = new Thread(() -> {
                a = 1;
                x = b;
                latch.countDown();
            });

            Thread other = new Thread(() -> {
                    b = 1;
                    y = a;
                    latch.countDown();
            });
            one.start();
            other.start();
            latch.await();
            String result = "第" + i + "次 (" + x + "," + y + ") ";
            if (x == 0 && y ==0) {
                //只有当x=b和y=a先执行后, 才会出现这种情况, 也就是乱序执行
                System.err.println(result);
                break;
            }
        }
    }
}

在这里插入图片描述

为什么会乱序?

简单来说, 是为了提高效率

CPU为了提高执行效率, 而做出的一种对执行顺序的优化。

在这里插入图片描述

乱序存在的条件

  • as-if-serial(看似顺序执行)
  • 不影响单线程的最终一致性
/**
 * @Description: 《Java并发编程实践》小程序
 * 有两大问题, 第一可见性问题,ready前应该加volatile修饰,第二,有序性问题
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 17:26
 */
public class T02_NoVisbility {

    private static boolean ready = false;
    //number初始值为0
    private static int number;

    private static class ReaderThread extends Thread{

        @Override
        public void run() {
            while (!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new ReaderThread();
        t.start();
        //理论上来说number = 42和ready = true因为没有依赖关系
        //所以有可能会乱序执行, 即先执行ready=true,再执行number =42, 这种情况下, 上面的输出就会是0
        number = 42;
        ready = true;
        t.join();
    }
}
/**
 * @version 1.0
 * @author cyc
 * @date 2021/7/29 17:43
 */
public class T03_ThisEscape {

    private int num = 8;

    public T03_ThisEscape(){
        new Thread(()->{
            System.out.println(num);
        }).start();
    }

    public static void main(String[] args) throws IOException {
        //这里有可能会输出中间状态0,
        new T03_ThisEscape();
        //确保以上线程执行完,再结束主程序
        System.in.read();
    }
}

原子性

/**
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * 运行下面的程序,并分析结果
 * @author cyc
 */
package com.cyc.juc.c_012_Volatile;

import java.util.ArrayList;
import java.util.List;

public class T04_VolatileNotSync {
   volatile int count = 0;
   void m() {
      for(int i=0; i<10000; i++) {
         count++;
      }
   }
   
   public static void main(String[] args) {
      T04_VolatileNotSync t = new T04_VolatileNotSync();
      
      List<Thread> threads = new ArrayList<Thread>();
      
      for(int i=0; i<10; i++) {
         threads.add(new Thread(t::m, "thread-"+i));
      }
      
      threads.forEach((o)->o.start());
      
      threads.forEach((o)->{
         try {
            o.join();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      });
      
      System.out.println(t.count);
      
      
   }
   
}

优化:

/**
 * 对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性
 * @author cyc
 */
package com.cyc.juc.c_012_Volatile;

import java.util.ArrayList;
import java.util.List;


public class T05_VolatileVsSync {
   /*volatile*/ int count = 0;

   synchronized void m() { 
      for (int i = 0; i < 10000; i++) {
         count++;
      }
   }

   public static void main(String[] args) {
      T05_VolatileVsSync t = new T05_VolatileVsSync();

      List<Thread> threads = new ArrayList<Thread>();

      for (int i = 0; i < 10; i++) {
         threads.add(new Thread(t::m, "thread-" + i));
      }

      threads.forEach((o) -> o.start());

      threads.forEach((o) -> {
         try {
            o.join();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      });

      System.out.println(t.count);

   }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

意田天

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

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

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

打赏作者

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

抵扣说明:

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

余额充值