Java学习笔记-多线程

多线程技术概述

线程与进程

进程

  • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间

线程

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程

  • 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

线程调度

分时调度

  • 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间(将各软件执行的时间平分,平分成很细小的时间,让人感知不到各软件切换的时间)。

抢占式调度

  • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

同步与异步

同步:排队执行,效率低但是线程安全

异步:同时执行,效率高但是数据不安全

并发与并行

并发:指两个或多个事件在同一个时间段内发生。一天发生了的事情,就是一天的并发量,一秒钟执行了多少件事情就是一秒的并发量。

并行:指两个或多个事件在同一时刻发生(同时发生)。服务器五千个用户并行执行,cpu的数量有限,不可能有如此高的并行执行量所以这个说法是错的。

多线程案例

通过创建Thread类的对象实现多线程

先创建一个线程类继承Thread类:

package com.java.demo;

public class MyThread extends Thread{

    /**
     * run方法就是线程执行的任务方法
     */
    @Override
    public void run() {
        //这里的代码 就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run方法,而是通过thread对象的start()来启动任务
        for(int i = 0; i < 10;i++){
            System.out.println("哈哈哈"+i);
        }
    }
}

在主函数中调用线程类的start()方法调用重写的run()方法内的内容和主函数的打印过程并发执行:

package com.java.demo;

public class Demo {
    /**
     * 多线程技术
     */
    public static void main(String[] args) {
        //Thread
        MyThread m = new MyThread();
        m.start();
        for(int i = 0; i < 10;i++){
            System.out.println("嘿嘿嘿"+i);
        }
    }
}

效果

有时线程会均匀分配如下图所示:
在这里插入图片描述

有时线程不会均匀分配如下图所示:
在这里插入图片描述
执行过程

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

通过实现Runnable来创建任务对象实现多线程

通过实现Runnable来创建任务对象,再创建Thread类的线程对象,然后为线程对象分配任务对象来实现多线程的案例:

先实现Runnable接口创建一个任务类:

package com.java.demo;

/**
 * 用于给线程进行执行的任务
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //线程的任务
        for(int i = 0; i < 10;i++){
            System.out.println("嘻嘻嘻"+i);
        }
    }
}

主函数代码:

package com.java.demo;

public class Demo {
    /**
     * 多线程技术
     * 实现Runnable 与 继承Thread相比有如下优势:
     * 1.   通过创建任务,然后给线程分配的方式来实现的多线程, 更适合多个线程同时执行相同任务的情况。
     * 2.   可以避免java单继承所带来的局限性,可以利用Runnable接口多实现的优势。
     * 3.   任务与线程本身时分离的,提高了程序的健壮性。
     * 4.   后续学习的线程池技术,接受且只管理Runnable类型的任务,不接受不管理Thread类型的线程。
     */
    public static void main(String[] args) {
        //实现Runnable
        //1.    创建一个任务对象
        MyRunnable r = new MyRunnable();
        //2.    创建一个线程,并为其分配一个任务
        Thread t = new Thread(r);
        //3.    执行这个线程
        t.start();
        for(int i = 0; i < 10;i++){
            System.out.println("嘿嘿嘿"+i);
        }
    }
}

效果:
在这里插入图片描述

实现Runnable 与 继承Thread相比有如下优势:

  1. 通过创建任务,然后给线程分配的方式来实现的多线程, 更适合多个线程同时执行相同任务的情况。
  2. 可以避免java单继承所带来的局限性,可以利用Runnable接口多实现的优势。
  3. 任务与线程本身时分离的,提高了程序的健壮性。
  4. 后续学习的线程池技术,接受且只管理Runnable类型的任务,不接受不管理Thread类的线程。

调用Thread匿名内部类的方法来实现多线程

代码:

package com.java.demo;

public class Demo2 {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for(int i = 0; i < 10;i++){
                    System.out.println("哈哈哈"+i);
                }
            }
        }.start();

        for(int i = 0; i < 10;i++){
            System.out.println("嘿嘿嘿"+i);
        }
    }
}

效果:
在这里插入图片描述

Thread类

构造方法:

构造器描述
Thread()分配新的 Thread对象。
Thread(Runnable target)分配新的 Thread对象。
Thread(Runnable target, String name)分配新的 Thread对象。
Thread(String name)分配新的 Thread对象。
Thread(ThreadGroup group, Runnable target)分配新的 Thread对象。

调用方法:

变量和类型方法描述
longgetId()返回此Thread的标识符。
StringgetName()返回此线程的名称。
int****getPriority()返回此线程的优先级。
ThreadGroupgetThreadGroup()返回此线程所属的线程组。
voidinterrupt()中断此线程。
static booleaninterrupted()测试当前线程是否已被中断。
booleanisAlive()测试此线程是否存活。
booleanisDaemon()测试此线程是否为守护程序线程。
booleanisInterrupted()测试此线程是否已被中断。
voidjoin()等待这个线程死亡。
voidjoin(long millis)此线程最多等待 millis毫秒。
voidjoin(long millis, int nanos)此线程最多等待 millis毫秒加上 nanos纳秒。
static voidonSpinWait()表示调用者暂时无法进展,直到其他活动发生一个或多个操作为止。
voidresume()已过时。 此方法仅适用于suspend() ,由于它易于死锁,因此已被弃用。
void****run()如果此线程是使用单独的Runnable运行对象构造的,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
voidsetDaemon(boolean on)将此线程标记为 daemon线程(守护线程)或用户线程。
voidsetName(String name)将此线程的名称更改为等于参数 name
voidsetPriority(int newPriority)更改此线程的优先级。
static void****sleep(long millis)导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数(参数值),具体取决于系统计时器和调度程序的精度和准确性。
static voidsleep(long millis, int nanos)导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。
void****start()导致此线程开始执行; Java虚拟机调用此线程的run方法。
StringtoString()返回此线程的字符串表示形式,包括线程的名称,优先级和线程组。
static voidyield()向调度程序提示当前线程是否愿意产生其当前使用的处理器。
voidstop()已过时。 这种方法本质上是不安全的。

什么叫做守护线程?

main方法是主线程,再启动的线程叫做子线程,即”后开启为子“。

线程真正在程序中划分的分类是:用户线程和守护线程

一个进程里面包括n个线程,如果一个线程都没了程序就会死亡,其实也可以如此去理解,如果main线程也就是主线程启动了一个线程a,那么如果主线程死了a线程没死程序就不会结束,事情还在做,那么这种a线程不管是主线程也好还是子线程也好我们都称之为用户线程,所有的用户线程必须全部死亡程序才会结束

而守护线程就不一样,它属于守护用户线程的一种线程,这个守护线程找不到自己的生命,守护线程活着不代表程序不能结束,用户线程是自己决定自己的死亡,而守护线程是当所有用户线程都死亡自己才发现自己没有守护对象一样才死亡,他是依附于用户线程的,有种**”人在塔在,人在塔不在了人活着也没用了“**的感觉。

字段汇总

setPriority(int newPriority)更改此线程的优先级的字段。

变量和类型字段描述
static intMAX_PRIORITY线程可以拥有的最大优先级。
static intMIN_PRIORITY线程可以拥有的最低优先级。
static intNORM_PRIORITY分配给线程的默认优先级。

设置和获取线程名称

代码:

package com.java.demo;

public class Demo3 {
    public static void main(String[] args) {
        //如何获取线程的名称
        System.out.println(Thread.currentThread().getName());
        new Thread(new MyRunnable(),"哈哈哈").start();
    }

    static class MyRunnable implements Runnable{

        @Override
        public void run() {
            //.currentThread()获取当前线程的对象
            System.out.println(Thread.currentThread().getName());
        }
    }
}

关键是使用了.currentThread()方法获取当前线程对象。

再使用.get().set()方法对对象进行操作。

效果:

在这里插入图片描述

如果不指定线程名称的代码:

package com.java.demo;

public class Demo3 {
    public static void main(String[] args) {
        //如何获取线程的名称
        System.out.println(Thread.currentThread().getName());
        new Thread(new MyRunnable()).start();
    }

    static class MyRunnable implements Runnable{

        @Override
        public void run() {
            //.currentThread()获取当前线程的对象
            System.out.println(Thread.currentThread().getName());
        }
    }
}

效果:

在这里插入图片描述

线程也会给自己一个默认的名称。多个线程默认名称效果如下:

在这里插入图片描述

线程休眠sleep

package com.java.demo;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        //线程的休眠 sleep
        for(int i = 0;i < 10;i++){
            System.out.println(i);
            Thread.sleep(1000);//1000ms = 1s
        }
    }
}

效果:执行时花费了至少10秒左右的时间。

线程阻塞

线程阻塞不只是线程休眠。

线程是一条执行路径,比如说它在执行代码的执行路径有100行,第1行到100行是整体的执行路径,这100行里可能中间有10行是为了读取某个文件资源的,读取文件消耗的时间比如有1s,那这1秒钟也是阻塞的,它停在那读文件,读完后才会继续执行。

可以把线程阻塞简单地理解为所有比较消耗时间的操作,比如说常见的文件读取,它会导致代码等待直至文件读取完毕才会往下执行,再比如说接收用户输入,它也会等待用户输入后才往下执行,用户不输入代码不会往下执行,我们也称线程阻塞为耗时操作。

线程的中断

面试时经常会问如何将一个线程停止?

虽然会有一个stop()方法,但是这个方法实质上是不会停止线程的,同时也是一个过时的方法,这个方法也是不安全的,因为有可能一个线程(是一个单独的执行路径)在做自己的事情,它的事件还没有做完,如果stop()将它关掉极有可能导致它正在使用某些资源而没有来得及释放,假设说正在进行io操作把这线程掐死了,导致资源没有被释放,会导致其他软件操作不了资源,那么这个时候就会出现资源依然被占用和产生内存垃圾的问题。

那么要线程停止怎么做呢?

应该通知线程要被停止了,线程接到相应通知之后再去自杀,自杀其实就是run方法直接return就可以了,那怎么通知它呢?可以定义一个标记变量,比如定义变量等于1,线程在执行过程中会观察这个变量1是否改变值,比如说发现它变成-1了,就让线程自己做一个return,把run()方法结束就可以了,作为一个合理的return将资源释放在结束方法就可以了。

如果main线程先结束子线程还未结束的代码:

package com.java.demo;

public class Demo6 {
    public static void main(String[] args) {
        //线程的中断
        //一个线城市一个独立的执行路径,它是否应该结束,应该由其自身决定
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for(int i = 0;i < 5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记
        t1.interrupt();

    }

    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for(int i = 0;i < 10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//线程中断异常
                    //e.printStackTrace();
                    System.out.println("发现了中断标记,但是我们就是不死亡");
                }
            }
        }
    }
}

效果:

在这里插入图片描述

可见主线程先结束了,子线程catch到异常时会输出异常,但如果我们没有让它自杀(中断),子线程还会执行下去的。

所以我们可以在catch块中加入return;实现线程中断。代码:

package com.java.demo;

public class Demo6 {
    public static void main(String[] args) {
        //线程的中断
        //一个线城市一个独立的执行路径,它是否应该结束,应该由其自身决定
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for(int i = 0;i < 5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记
        t1.interrupt();

    }

    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for(int i = 0;i < 10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//线程中断异常
                    //e.printStackTrace();
                    System.out.println("发现了中断标记,线程自杀");
                    return;
                }
            }
        }
    }
}

效果:
在这里插入图片描述

守护线程

线程:分为守护线程和用户线程

  • 用户线程:当一个进程不包含任何的存活的用户线程时,进行结束。

  • 守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。

直接创建的都是用户线程,使用线程名.setDaemon(true);就可以将线程设为守护线程。

package com.java.demo;

public class Demo6 {
    public static void main(String[] args) {
        //线程:分为守护线程和用户线程
        //用户线程:当一个进程不包含任何的存活的用户线程时,进行结束。
        //守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
        //直接创建的都是用户线程
        Thread t1 = new Thread(new MyRunnable());
        //设置t1为守护线程
        t1.setDaemon(true);
        t1.start();
        for(int i = 0;i < 5;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记
        t1.interrupt();

    }

    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for(int i = 0;i < 10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//线程中断异常
                    e.printStackTrace();
                    return;
                }
            }
        }
    }
}

效果:

在这里插入图片描述

线程安全问题

线程不安全的卖票案例:

package com.java.demo;

public class Demo7 {
    public static void main(String[] args) {
        //线程不安全
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        @Override
        public void run() {
            while (count>0){
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println("出票成功,余票:"+count);
            }
        }
    }
}

效果:

在这里插入图片描述

逻辑上不应该出现余票为负数。当三个线程都刚好卡到count刚好大于0而能进入循环的时间片时三者都会执行一次循环体的内容,再次进入发现count小于等于0时才中断自己。

线程安全处理

线程安全1-同步代码块

格式:

synchronized(锁对象){
    同步的代码块
}

锁对象:

java中任何对象都也可以通过上面的格式被标记为锁对象。

工作原理:

当多个线程再次涌入同步代码块时,它们发现有这个锁对象标记,那么它们就会进行等待。等锁对象运行完成后,后面来的线程就会发现锁解开了就会同时去争抢这个锁,所以抢到以后呢就会再次给锁打上标记,然后继续由新的线程去执行同步的代码块。

注意:

多个线程得共同面对一把锁,这样才会实现排队机制。

案例代码:

package com.java.demo;

public class Demo7 {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案1.同步代码块
        //格式: synchronized(锁对象){
        //
        //      }
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
            /*Object o = new Object();//如果锁对象放在这里,这时会导致三个线程各看自己的锁对象,就会导致三个线程各排各的队,最终导致锁失效*/
            while (true) {
                synchronized (o) {//谁先抢到谁再次抢到的概率会更高(回手掏概率非常高)
                    if (count > 0) {
                        //卖票
                        System.out.println("正在准备卖票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    }else {
                        break;
                    }
                }
            }
        }
    }
}

效果:

在这里插入图片描述

可以发现不会再出现余票为负数的情况了,证明三个线程有进行排队抢票,但是Thread-0最先抢到锁的同步代码块,由于谁先抢到谁再次抢到的概率会更高(回手掏概率非常高),所以会发生Thread-0一直都能抢到票。

线程安全2-同步方法

案例代码:

package com.java.demo;

public class Demo7 {
    /**
     * 线程同步:synchronized
     * @param args
     */
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案2. 同步方法:在方法前用synchronized修饰即可。
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        @Override
        public void run() {
            while (true) {
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }
        }

        public synchronized boolean sale(){
            if (count > 0) {
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                return true;
            }else {
                return false;
            }
        }
    }
}

效果:

在这里插入图片描述

注意:

  1. 线程同步方法的锁是谁,方法也是由锁来进行控制的。如果不是静态方法的锁就是this。如果是静态方法(用static修饰的)的锁是类名.class(字节码文件对象)。
  2. **如果是同步代码块锁了一段代码,同步方法呢又锁了另外一个代码,那么这两使用了同一个锁对象都是this,那么线程先抢到同步代码块后,其他线程也无法执行同步方法。**可以理解为多个区域(代码)只有一个门可以进入,有人进入后,门锁上了,没有人在可以进入这多个区域。

线程安全3-显式锁Lock

案例代码:

package com.java.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo7 {
    /**
     * 同步代码块 和 同步方法 都属于隐式锁
     * 线程同步:Lock
     * @param args
     */
    public static void main(String[] args) {
        //线程不安全
        //解决方案2. 显示锁 Lock 子类 ReentrantLock
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        //显式锁
        private Lock lock = new ReentrantLock();
        @Override
        public void run() {
            while (true) {
                lock.lock();
                if (count > 0) {
                    //卖票
                    System.out.println("正在准备卖票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                }else {
                    break;
                }
                lock.unlock();
            }
        }
    }
}

效果:

在这里插入图片描述

公平锁与非公平锁

公平锁:先来先到,真的排队机制。

非公平锁:线程都在抢,上面学的同步代码块、同步方法和显示锁都是非公平锁。

那么如何实现公平锁?

在显示锁的基础上,在创建锁对象时在构造方法的参数处写true,如此就可以表示公平锁。

案例代码:

package com.java.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo7 {
    /**
     * 同步代码块 和 同步方法 都属于隐式锁
     * 线程同步:Lock
     * @param args
     */
    public static void main(String[] args) {
        //线程不安全
        //解决方案2. 显示锁 Lock 子类 ReentrantLock
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        //显式锁 lock : fair参数为true 就表示时公平锁。
        private Lock lock = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                lock.lock();
                if (count > 0) {
                    //卖票
                    System.out.println("正在准备卖票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                }else {
                    break;
                }
                lock.unlock();
            }
        }
    }
}

效果:

在这里插入图片描述

线程死锁

概念:

案例代码:

package com.java.demo;

public class Demo8 {
    public static void main(String[] args) {
        //线程死锁
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);
    }
    static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        public MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);
        }
    }

    //罪犯
    static class Culprit{
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放人质");
            p.fun();
        }
        public synchronized void fun(){
            System.out.println("罪犯被放了,罪犯也放了人质");
        }
    }
    //警察
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救了人质,罪犯也跑了");
        }
    }
}

死锁卡死效果:

在这里插入图片描述

​ 当罪犯c调用say()方法时,把警察p对象传入进去之后需要调用警察的回应fun方法,所以罪犯c的say方法没执行完,此时罪犯的调用罪犯say()的线程是锁住的,结果警察的子线程的运行也调用了警察p的say方法,警察也需要调用罪犯的回应fun方法,所以警察p的say方法没执行完,此时警察的调用警察say()的线程也是锁住的,首先呢罪犯想要等到警察的回应fun方法结束自己的say方法,这时却又需要警察必须执行完say方法,但是警察执行了say也要等待罪犯的fun,可是罪犯也还没有结束say方法,结果就这样卡死了。

也有没形成死锁卡死的效果:

在这里插入图片描述

​ 这时是因为主线程执行的主线程的c.say(p);在子线程new MyThread(c,p).start();调用run()方法之前抢先调用完成执行了p.fun()了,这时就完成c.say(p);了,所以调用run()方法时p.say(c);就可以继续往下执行,执行p.fun()得到回应了。

如何避免死锁呢?

​ 很简单,只要是调用执行了有锁的方法,就不让它在调用其他有锁的方法,而是调用无锁的方法就可以了。

多线程通信问题

场景:

​ A线程去网上下载音乐,B线程在下载好音乐之后进行播放,那么A线程下载好之后就要通知B线程来播放音乐,因为这两个线程是独立的,这个时候就需要多线程通信。

​ 这个时候A线程就是生产者,B线程就是消费者。

如何实现多线程通信呢?

​ 我们需要看向Object类的方法API中的wait()方法和notify()方法。

变量和类型方法描述
voidnotify()唤醒正在此对象监视器上等待的单个线程。
voidnotifyAll()唤醒等待此对象监视器的所有线程。
voidwait()导致当前线程等待它被唤醒,通常是 通知中断
voidwait(long timeoutMillis)导致当前线程等待它被唤醒,通常是 通知中断 ,或者直到经过一定量的实时。
voidwait(long timeoutMillis, int nanos)导致当前线程等待它被唤醒,通常是 通知中断 ,或者直到经过一定量的实时。

生产者与消费者

就好像一件饭店只有一个上菜盘子,厨师和服务员之间如同两个线程关系一样,厨师做完菜放入盘子后服务员才可以端走上菜,那服务员等厨师做完菜放入盘子之前就执行wait(),将上菜这个动作先暂时休眠,然后等厨师做完菜之后放入盘子在执行notifyAll()或者notify()方法唤醒服务员上菜的动作,让服务员上菜,这时厨师干完事情要等盘子回来之后才能继续做菜,在等的时候自己要执行wait()就会自己休息不执行做菜,等顾客吃完了之后服务员把盘子洗干净带回来时执行notifyAll()或者notify()方法唤醒厨师做菜的动作。

没使用线程安全时的案例代码:

package com.java.demo;

public class Demo9 {
    /**
     * 多线程通信问题,生产者和消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if (i%2==0){
                    f.setNameAndTaste("糖醋鱼","齁甜");
                }else{
                    f.setNameAndTaste("一夜埕","臭咸");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //菜品
    static class Food{
        private String name;
        private String taste;
        public void setNameAndTaste(String name,String taste){
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }
        private void get(){
            System.out.println("服务员端走的菜的名称是"+name+",味道:"+taste);
        }
    }
}

效果:

在这里插入图片描述

这时上菜虽然交替但是味道错了,笑死。

使用了线程安全但是没有进行线程通信的案例代码:

package com.java.demo;

public class Demo9 {
    /**
     * 多线程通信问题,生产者和消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if (i%2==0){
                    f.setNameAndTaste("糖醋鱼","齁甜");
                }else{
                    f.setNameAndTaste("一夜埕","臭咸");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //菜品
    static class Food{
        private String name;
        private String taste;
        public synchronized void setNameAndTaste(String name,String taste){
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }
        private synchronized void get(){
            System.out.println("服务员端走的菜的名称是"+name+",味道:"+taste);
        }
    }
}

效果:
在这里插入图片描述

这时上菜味道没错,但是由于执行了服务员端菜的线程在抢着运行,所以厨师做好的一到菜反复端着了,笑死。

使用了线程安全也进行线程通信的案例代码:

package com.java.demo;

public class Demo9 {
    /**
     * 多线程通信问题,生产者和消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if (i%2==0){
                    f.setNameAndTaste("糖醋鱼","齁甜");
                }else{
                    f.setNameAndTaste("一夜埕","臭咸");
                }
            }
        }
    }
    //服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //菜品
    static class Food{
        private String name;
        private String taste;
        //true表示可以生产,flase的时候要服务员把盘子端回来
        private boolean flag = true;

        public synchronized void setNameAndTaste(String name,String taste){
            if(flag){
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        private synchronized void get(){
            if(!flag){
                System.out.println("服务员端走的菜的名称是"+name+",味道:"+taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

效果:

在这里插入图片描述

上菜交替且味道正确了。

线程的六种状态

public static enum Thread.State extends Enum<Thread.State>

线程状态。 线程可以处于以下状态之一:

  • NEW
    线程刚被创建但尚未启动的线程处于此状态。
  • RUNNABLE
    在Java虚拟机中执行的线程处于此状态。
  • BLOCKED
    被阻塞等待监视器锁定的线程处于此状态,当一个线程和别的线程排队时因为线程安全而被阻塞的状态。
  • WAITING
    无限等待,无限期等待另一个线程执行特定操作的线程处于此状态。
  • TIMED_WAITING
    计时等待,正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
  • TERMINATED
    线程终止,已退出的线程处于此状态。

线程在给定时间点只能处于一种状态。 这些状态是虚拟机状态,不反映任何操作系统线程状态。

在这里插入图片描述

带返回值的线程Callable

Runnable Callable

接口定义 
//Callable接口 
public interface Callable<V> {
    V call() throws Exception; 
}
//Runnable接口 
public interface Runnable {
    public abstract void run(); 
}

Callable使用步骤

1. 编写类实现Callable接口指定返回泛型, 实现call方法 
    class XXX implements Callable<T> {
        @Override 
        public <T> call() throws Exception {
            return T; 
        } 
    } 
2. 创建FutureTask任务对象 , 并传入第一步编写的Callable类对象 
    FutureTask<Integer> future = new FutureTask<>(callable); 
3. 通过Thread,启动线程 
    new Thread(future).start();

FutureTask任务对象

调用方法:

变量和类型方法描述
protected voiddone()当此任务转换到状态 isDone (无论是正常还是通过取消),调用受保护的方法。
Vget()如果需要等待计算完成,然后检索其结果。
Vget(long timeout, TimeUnit unit)如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。
protected booleanrunAndReset()执行计算而不设置其结果,然后将此未来重置为初始状态,如果计算遇到异常或被取消则无法执行此操作。
protected voidset(V v)将此future的结果设置为给定值,除非已设置或已取消此未来。
protected voidsetException(Throwable t)导致此未来报告带有给定throwable的ExecutionException作为其原因,除非此未来已设置或已取消。
StringtoString()返回此FutureTask的字符串表示形式。

案例代码:

package com.java.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo10 {
    //java中第三种实现线程的方式Callable
    public static void main(String[] args) {
        Callable<Integer> c = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(c);
        /*task.isDone();//可以用来判断子线程是否已经结束*/
        /*task.cancel(true);//取消子线程执行情况*/
        new Thread(task).start();
        Integer j = null;
        try {
            j = task.get();//主线程会等待子线程运行完后再执行自己的代码
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(j);
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }

    static class MyCallable implements Callable<Integer>{

        @Override
        public Integer call() throws Exception {
            //Thread.sleep(3000);
            for(int i=0;i<10;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
            return 100;
        }
    }
}

效果:

在这里插入图片描述

Runnable与Callable的相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

Runnable与Callable的不同点

  • Runnable没有返回值;Callable可以返回执行结果
  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出

Callable获取返回值

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程池 Executors

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

定长线程池

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

定长线程池中,当线程数组内所有的线程都忙碌时,任务列表其他任务都会在外面排队等待,等到县城数组中有线程空闲时便会依次进入线程数组被执行。

可变长线程池

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

可变长线程池中,当线程数组内所有的线程都忙碌时,就会动态扩容,当发现不会有线程经常空闲时,就会自己删除缓存的线程,就如服务器中访问的线程在早上会高达800的访问量,当访问线程在晚上的访问量只会有80,这时可变长线程池就会有一定的作用场景。

操作步骤:

package com.java.demo;

public class Demo11 {
    //线程池
    public static void main(String[] args) {
        //创建线程

        //创建任务

        //执行任务(耗时占5%,所以频繁创建线程、创建任务、关闭线程会导致程序效率)

        //关闭线程
    }
}

线程池的好处

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

Java中的四种线程池 . ExecutorService

1. 缓存线程池

	/**
	* 缓存线程池. 
	* (长度无限制) 
	* 执行流程: 
	* 1. 判断线程池是否存在空闲线程 
	* 2. 存在则使用 
	* 3. 不存在,则创建线程 并放入线程池, 然后使用 
	*/ 
	ExecutorService service = Executors.newCachedThreadPool(); 
	//向线程池中 加入 新的任务 
	service.execute(new Runnable() {
    	@Override 
    	public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    	} 
	});
	service.execute(new Runnable() {
    	@Override public void run() { 
        	System.out.println("线程的名称:"+Thread.currentThread().getName());
    	} 
	}); 
	service.execute(new Runnable() {
    	@Override 
    	public void run() {
        	System.out.println("线程的名称:"+Thread.currentThread().getName()); 
    	} 
	});

案例:

未加入休眠的代码:

package com.java.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo11 {
    /**
     * 缓存线程池
     * (长度无限制)
     * 任务加入后的执行流程
     *      1. 判断线程池是否存在空闲线程
     *      2. 存在则使用
     *      3. 不存在,则创建线程 并放入线程池,然后使用
     */
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
    }
}

效果:

在这里插入图片描述

没有休眠时,所有任务都在同一个时间执行,即使可能线程池中的线程都忙碌了,但是缓存线程池会在全部线程忙碌且有新的任务需要通过缓存线程池执行时就会动态创建线程。

加入休眠后的代码:

package com.java.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo11 {
    /**
     * 缓存线程池
     * (长度无限制)
     * 任务加入后的执行流程
     *      1. 判断线程池是否存在空闲线程
     *      2. 存在则使用
     *      3. 不存在,则创建线程 并放入线程池,然后使用
     */
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"哈哈哈哈哈");
            }
        });
    }
}

效果:

在这里插入图片描述

通过休眠的案例,我们可以看见执行当部分任务执行完之后就会让线程池空出位置,使得部分任务可以通过线程池内同一个线程中执行。

2. 定长线程池

	/*** 定长线程池. 
	* (长度是指定的数值) 
	* 执行流程:
	* 1. 判断线程池是否存在空闲线程 
	* 2. 存在则使用 
	* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 
	* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 
	*/ 
	ExecutorService service = Executors.newFixedThreadPool(2); 
	service.execute(new Runnable() {
        @Override 
        public void run() {
            System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    }); 
	service.execute(new Runnable() { 
        @Override 
        public void run() {
            System.out.println("线程的名称:"+Thread.currentThread().getName()); 
        } 
    });

案例代码:

package com.java.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo12 {
    public static void main(String[] args) {
        /*** 定长线程池.
         * (长度是指定的数值)
         * 执行流程:
         * 1. 判断线程池是否存在空闲线程
         * 2. 存在则使用
         * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
         * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
         */
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
    }
}

效果:

在这里插入图片描述

在任务超出线程池的线程数量时,线程池的线程都在工作的情况下,后面来的任务就会登台线程空闲再进去执行。

3. 单线程线程池

效果与定长线程池 创建时传入数值1 效果一致. 
    /**
    * 单线程线程池. 
    * 执行流程: 
    * 1. 判断线程池 的那个线程 是否空闲
    * 2. 空闲则使用 
    * 4. 不空闲,则等待 池中的单个线程空闲后 使用 */
    ExecutorService service = Executors.newSingleThreadExecutor();
	service.execute(new Runnable() {
        @Override 
        public void run() {
            System.out.println("线程的名称:"+Thread.currentThread().getName()); 
        } 
    }); 
	service.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程的名称:"+Thread.currentThread().getName());
        } 
    });

效果:

在这里插入图片描述

只有一个线程的线程池,所有的任务都只会用这一个线程通过排队进行执行。但是可以发现任务执行完之后程序并没有关闭,这是因为线程池还在等待接收任务,和用户线程没有关闭的情况一样,如此一来应用程序就不会关闭,在一定时间之后也会自己关闭。

4.周期性任务定长线程池

public static void main(String[] args) {
    /**
    * 周期任务 定长线程池. 
    * 执行流程: 
    * 1. 判断线程池是否存在空闲线程 
    * 2. 存在则使用 
    * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
    * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 
    *
    * 周期性任务执行时: 
    * 定时执行, 当某个时机触发时, 自动执行某任务 .
    */ 
    ScheduledExecutorService service = Executors.newScheduledThreadPool(2); 
    /**
    * 定时执行 
    * 参数1. runnable类型的任务 
    * 参数2. 时长数字 
    * 参数3. 时长数字的单位 
    */ 
    
    /*
    service.schedule(new Runnable() {
    	@Override 
    	public void run() {
        	System.out.println("俩人相视一笑~ 嘿嘿嘿"); 
        } 
    },5,TimeUnit.SECONDS);
    */
    
    /**
    * 周期执行 
    * 参数1. runnable类型的任务 
    * 参数2. 时长数字(延迟执行的时长) 
    * 参数3. 周期时长(每次执行的间隔时间) 
    * 参数4. 时长数字的单位 
    */ 
    service.scheduleAtFixedRate(new Runnable() {
        @Override public void run() {
            System.out.println("俩人相视一笑~ 嘿嘿嘿"); 
        } 
    },5,2,TimeUnit.SECONDS); 
}

定时执行案例代码:

package com.java.demo;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo14 {
    public static void main(String[] args) {
        /**
         * 周期任务 定长线程池.
         * 执行流程:
         * 1. 判断线程池是否存在空闲线程
         * 2. 存在则使用
         * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
         * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
         *
         * 周期性任务执行时:
         * 定时执行, 当某个时机触发时, 自动执行某任务 .
         */
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         * 定时执行
         * 参数1. runnable类型的任务
         * 参数2. 时长数字
         * 参数3. 时长数字的单位
         */

        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("俩人相视一笑~ 嘿嘿嘿");
            }
        },5, TimeUnit.SECONDS);//任务五秒后执行
    }
}

效果:

在这里插入图片描述

周期执行案例代码:

package com.java.demo;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo15 {
    public static void main(String[] args) {
        /**
         * 周期任务 定长线程池.
         * 执行流程:
         * 1. 判断线程池是否存在空闲线程
         * 2. 存在则使用
         * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
         * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
         *
         * 周期性任务执行时:
         * 定时执行, 当某个时机触发时, 自动执行某任务 .
         */
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

        /**
         * 周期执行
         * 参数1. runnable类型的任务
         * 参数2. 时长数字(延迟执行的时长)
         * 参数3. 周期时长(每次执行的间隔时间)
         * 参数4. 时长数字的单位
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override public void run() {
                System.out.println("俩人相视一笑~ 嘿嘿嘿");
            }
        },5,2, TimeUnit.SECONDS);
    }
}

效果:

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

周期线程池周期为5s不断重复执行任务,每个五秒钟打印了一次。

Lambda表达式

Thread t = new Thread(() -> System.out.println("哈哈哈哈"));

箭头左边小括号可放入参数,箭头右边的是方法执行内容

package com.java.demo;

public class Demo16 {
    /**
     * Lambda表达式
     * 函数式编程思想: 重点关注方法,不关注过程只注重结果。
     *  面向对象:   创建对象调用方法 解决问题。
     * @param args
     */
    public static void main(String[] args) {
        /*//冗余的Runnable代码
        //1.    创建类之后调用对象
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread(r);
        //2.    使用了匿名对象
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("哈哈哈哈");
            }
        });
        t1.start();
        t2.start();*/
        //使用Lambda表达式
        Thread t = new Thread(() -> System.out.println("哈哈哈哈"));
        t.start();
    }

    static class MyRunnable implements Runnable{

        @Override
        public void run() {
            System.out.println("哈哈哈哈");
        }
    }
}

效果:

在这里插入图片描述

使用Lambda表达式和使用面向对象实现Runnable接口创建创建线程类调用任务达成的效果是一致的。

不用Lambda表达式案例代码:

package com.java.demo;

public class Demo17 {
    public static void main(String[] args) {
        print(new MyMath() {
            @Override
            public int sum(int x, int y) {
                return x+y;
            }
        },100,200);
    }

    public static void print(MyMath m,int x,int y){
        int num = m.sum(x,y);
        System.out.println(num);
    }

    static interface MyMath{
        int sum(int x,int y);
    }
}

效果:

在这里插入图片描述

改用Lambda表达式案例代码:

package com.java.demo;

public class Demo17 {
    public static void main(String[] args) {
        print((int x,int y) -> {
            return x+y;
        },100,200);
    }

    public static void print(MyMath m,int x,int y){
        int num = m.sum(x,y);
        System.out.println(num);
    }

    static interface MyMath{
        int sum(int x,int y);
    }
}

效果:

在这里插入图片描述

在参数传入接口的这个匿名内部类时可以考虑使用Lambda表达式来代替,效果是可以达成一致的,代码也没有这么冗余。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值