这篇 Java 多线程,你一定能看懂学会!

前几天学习了 Java 多线程,作为一个懒癌后期的患者,一直拖到现在才把所学的记录下来,也算是复习了一遍 😂。希望大家多多支持喔!

在学习线程之前,我们先来了解一下进程吧!

进程

概述:正在运行的程序就是进程。进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
通过任务管理器,我们可以看到我们电脑现在的进程有哪些:
进程
多进程的意义:计算机可以在一个时间段内同时执行多个任务,可以提高CPU的使用率。
思考:我们一边听音乐(网易云进程),一边写代码(IDEA进程),这两个任务是同时的吗?
对于多核CPU 它有可能是同时的,但是对于单核CPU来说,它在某一个时间点,它只能做一件事情。
但是我们在听音乐的时候,同时在写代码,我们感官上,这两个任务是同时进行的。但是实际CPU在运行进程的时候进行了程序间的高速切换,这个切换时间非常的短,所以我们就感觉两个进程是在同时进行的。

线程

在同一个进程中,可以同时执行多个任务。而这每一个任务,就是一个线程。

线程:是程序的执行单元,也是执行路径。线程是程序使用CPU资源的最基本单位。
单线程:只有一个执行单元或只有一条执行路径
多线程:有多个执行单元或多个执行路径

例如:我们平时写的这些程序使单线程的

public class Test {
    public static void main(String[] args) {
        System.out.println("代码块1");
        method();
        System.out.println("代码块2");
    }
    public static void method() {
        System.out.println("代码块3");
        function1();
        function2();
        System.out.println("代码块4");
    }

    private static void function1() {
    }
    private static void function2() {
    }
}

线程

多线程的意义:

  1. 线程的执行是抢占式的。每一个线程都要去抢占CPU资源(CPU执行权)。一个多线程的程序在执行时,如果一些线程必须等待的时候,CPU就会将资源提供给其他线程去使用这些资源。这样的话就提高了CPU的使用率。
  2. 对于进程来说,如果它是多线程的,在抢占CPU资源时,就有更大的几率抢占到CPU资源。提高该程序使用率。

多线程

实现方式一:
继承 Thread 类,重写 run() 方法。
步骤:
1 自定义 MyThread 类,继承 Thread 类
2 重写 run() 方法
3 创建 MyThread 对象
4 启动线程

public class MyThread extends Thread {
	// 重写 run() 方法
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println(i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
    	// 创建 MyThread 对象
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }
}

注:run() 和 start() 的区别是什么?
run():仅仅封装了线程所执行的代码,直接调用和普通方法没有区别。
start():首先启动线程,然后由 JVM 调用该线程的 run() 方法。

  1. 获取线程名称:
    public final String getName():返回此线程的名称。
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println(getName() + i);
        }
    }
}
  1. 设置线程名称:
    法一:
    public final void setName(String name):将此线程的名称更改为参数 name
myThread.setName("线程一");

法二:构造方法

public class MyThread extends Thread {
    public MyThread() {
    }
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println(getName() + i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        // 使用有参构造:
        MyThread myThread = new MyThread("线程一:");
        myThread.start();

        MyThread myThread1 = new MyThread("线程二:");
        myThread1.start();
    }
}
  1. 获取当前正在执行的线程名称:
    public static Thread currentThread():返回对当前正在执行的线程对象的引用
String name = Thread.currentThread().getName();
System.out.println(name);
  1. 线程优先级:
    线程有两种调度模型:
    1、分时调度模型:所有的线程轮流使用 CPU,平均分配每个线程占用 CPU 的时间段。
    2、抢占式调度模型:Java 是抢占式调度模型,会优先让优先级高的执行;优先级相同的线程,随机执行一个。(注:优先级高只代表它抢到 CPU 的概率较大,不一定必须是先执行的)

获取优先级的方法:
public final int getPriority():返回此线程的优先级
设置优先级的方法:
public final int setPriority(int newPriority):设置此线程的优先级

// 设置线程优先级。
myThread1.setPriority(10);
myThread3.setPriority(1);

// 获取线程优先级。
System.out.println(myThread1.getPriority());
System.out.println(myThread2.getPriority());
System.out.println(myThread3.getPriority());
/*
输出的结果:
	10
	5
	1
*/

注:1. 默认优先级是5
2. 优先级的取值范围是 1-10

线程控制

  1. sleep():线程睡眠
    public static void sleep(long miles):导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数
public class MyThread3 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("有异常!");
            }
            System.out.println(getName() + "-" + i);
        }
    }
}
public class MyThreadDemo3 {
    public static void main(String[] args) {
        MyThread3 myThread1 = new MyThread3();
        MyThread3 myThread2 = new MyThread3();
        MyThread3 myThread3 = new MyThread3();

        myThread1.setName("喜羊羊");
        myThread2.setName("美羊羊");
        myThread3.setName("灰太狼");

        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}

执行上面的程序,可以看到每个线程都是输出一次之后等待一秒再继续输入

  1. interrupt():线程中断
    public void interrupt():中断线程
public class MyThread7 extends Thread {
    @Override
    public void run() {
        System.out.println("线程开始执行" + new Date());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("程序出现错误!");
        }
        System.out.println("线程结束执行" + new Date());
    }
}
public class MyThreadDemo7 {
    public static void main(String[] args) {
        MyThread7 myThread1 = new MyThread7();

        //启动线程
        myThread1.start();

        // myThread1 休眠时间超过三秒,就终止它。
        try {
            // 主线程休眠三秒。
            Thread.sleep(3000);
            myThread1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程中断

  1. join():线程加入
    public void join():等待该线程终止
public class MyThread4 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class MyThreadDemo4 {
    public static void main(String[] args) {
        // 创建线程类对象
        MyThread4 myThread1 = new MyThread4();
        MyThread4 myThread2 = new MyThread4();
        MyThread4 myThread3 = new MyThread4();

        // 设置名称。
        myThread1.setName("线程一");
        myThread2.setName("线程二");
        myThread3.setName("线程三");

        myThread1.start();
        // 加入线程。
        try {
            myThread1.join();
        } catch (InterruptedException e) {
            System.out.println("出错了!");
        }
        myThread2.start();
        myThread3.start();
    }
}

执行上面程序,线程一执行完毕后,后面两个线程才开始抢占资源,进行执行

  1. yield():线程礼让
    public static void yield():暂停当前正在执行的线程对象,并执行其他线程(可以减小抢占竞争)
public class MyThread5 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(getName() + ": " + i);
            // 线程礼让
            Thread.yield();
        }
    }
}
public class MyThreadDemo05 {
    public static void main(String[] args) {
        // 创建线程类对象
        MyThread5 myThread1 = new MyThread5();
        MyThread5 myThread2 = new MyThread5();

        // 设置名称。
        myThread1.setName("Andy");
        myThread2.setName("Jay");

        myThread1.start();
        myThread2.start();
    }
}
  1. setDeman():线程守护
    public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程(当正在运行的线程都是守护线程时,Java 虚拟机退出,程序结束)
public class MyThread6 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class MyThreadDemo6 {
    public static void main(String[] args) {
        MyThread6 myThread1 = new MyThread6();
        MyThread6 myThread2 = new MyThread6();

        myThread1.setName("线程一");
        myThread2.setName("线程二");

        // 守护线程设置为true。主基地结束,其余两个线程一会也会结束。
        myThread1.setDaemon(true);
        myThread2.setDaemon(true);

        myThread1.start();
        myThread2.start();

        // 获取主线程。
        Thread.currentThread().setName("主基地:");
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}

线程的生命周期:
生命周期

实现方式二:
实现 Runnable 接口,重写 run() 方法。然后可以分配类的实例,在创建 Thread 时作为参数传递。
步骤:
1.创建自定义线程类。
2.重写 run 方法
3.创建自定义线程类对象
4.创建多个Thread类对象,将自定义线程类对象作为参数传递。
5.启动线程。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
        	// getName() 是Thread的方法,所以在这里应该这样获取线程名称
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
    	// 创建自定义线程类对象
        MyRunnable mr = new MyRunnable();
		
		// 创建多个Thread类对象,将自定义线程类对象作为参数传递
        Thread t1 = new Thread(mr, "线程一");
        Thread t2 = new Thread(mr, "线程二");

		// 启动线程
        t1.start();
        t2.start();
    }
}

注:为什么创建线程的第一种方法还要有第二种方法?

  1. 继承只能单继承,如果自定义线程类有父类,则它不能再继承Thread
  2. 第二种方式适合多个线程操作同一个资源这种情况,比较简洁。把线程和程序代码 数据进行有效分离,较好地体现了面向对象的思想。

案例:共有150张票,创建三个线程,模拟电影院三个窗口的卖票情况。

public class MyRunnable implements Runnable {
	// 如果这里是继承 Thread 类,则需要用static来修饰票数,以保证三个线程共享同一个资源,三个窗口共卖这150张票。
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                	// 根据现实情况,卖票会出现延迟
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "张票。");
            }
        }
    }
}

执行上面程序,会发现有一票多卖和负数票的情况,该问题与线程安全有关。

线程安全:

出现原因:

  1. 多线程环境
  2. 存在共享数据
  3. 存在多条语句操作该共享数据

为了解决这个问题可以将操作共享数据的这段代码包裹起来,在有线程访问这段代码时,其他线程不能访问。

同步代码块:
synchronize 关键字:

synchronize(对象名){
​		多条语句;
}

同步代码块的对象是任意对象。
如果给这些线程传递同一个对象,就是相当于给了一个门,一把锁;如果传递不同的对象,就相当于有多个门,多把锁。

public class MyRunnable implements Runnable {
    private int tickets = 50;
    private Object object = new Object();
    @Override
    public void run() {

        while (true) {
            synchronized (object) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出去标号为 " + (tickets--) + "的这张票");
                }
            }
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr, "窗口一:");
        Thread t2 = new Thread(mr, "窗口二:");
        Thread t3 = new Thread(mr, "窗口三:");

        t1.start();
        t2.start();
        t3.start();
    }
}

注:如果使用继承 Thread 类的方法,则需要将票数和锁对象声明为静态的,以保证为所有对象共用。

private static int ticket = 50;
private static Object obj = new Object();

执行以上代码,上面的一票多卖和负数票的问题都被解决了。

同步代码块的优缺点:
优点:解决了线程安全问题。
缺点:每个线程在执行前都要去判断锁对象,无形中增加了电脑负担。

同步方法:
格式一:synchronized 权限修饰符 返回值类型 方法名()
格式二:权限修饰符 synchronized 返回值类型 方法名()

public class SaleTicket implements Runnable {
    private int ticket = 50;
    @Override
    public void run() {
        ticket();
    }

    private synchronized void ticket() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "张票");
            }
        }
    }
}
public class SaleTicketDemo {
    public static void main(String[] args) {
        SaleTicket st = new SaleTicket();

        Thread t1 = new Thread(st, "窗口一:");
        Thread t2 = new Thread(st, "窗口一:");
        Thread t3 = new Thread(st, "窗口一:");

        t1.start();
        t2.start();
        t3.start();
    }
}

同步方法的锁对象是this,而如果该同步方法是静态的,由于静态方法随着类的加载而加载,那么它的锁对象是该类的字节码文件(类名.class)。

Lock锁
Lock:这是一个接口,实现了比 synchronize 更语句和方法更广泛的操作。
实现子类:ReentrantLock
成员方法:void lock() 上锁;void unlock 解锁。

public class SaleTicket implements Runnable {
    private int tickets = 50;
    // 使用多态的方法创建锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 上锁。
            lock.lock();
            if (tickets > 0) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出去标号为 " + (tickets--) + "的这张票");
            }
            // 解锁
            lock.unlock();
        }
    }
}
public class SaleTicketDemo {
    public static void main(String[] args) {
        SaleTicket s = new SaleTicket();

        Thread thread1 = new Thread(s, "窗口一:");
        Thread thread2 = new Thread(s, "窗口二:");
        Thread thread3 = new Thread(s, "窗口三:");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

注:上锁和解锁的位置和同步代码块的位置相同。

死锁:
同步代码块的弊端:效率低,而且如果出现了同步嵌套,就容易产生死锁的问题。
死锁:是指两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

public class deadLock extends Thread {
    private boolean flag;
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    // 构造方法
    public deadLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (lockA) {
                System.out.println("if lockA");
                synchronized (lockB) {
                    System.out.println("if lockB");
                }
            }
        } else {
            synchronized (lockB) {
                System.out.println("else lockB");
                synchronized (lockA) {
                    System.out.println("else lockA");
                }
            }
        }
    }
}
public class deadLockDemo {
    public static void main(String[] args) {
        deadLock deadLock1 = new deadLock(true);
        deadLock deadLock2 = new deadLock(false);

        deadLock1.start();
        deadLock2.start();
    }
}

生产者消费者模型
线程间通讯:不同种类的线程针对同一个资源进行操作

例如:不同线程操作同一个学生对象,一个线程用来设置学生对象,一个线程用来获取学生对象。

public class Student {
    private String name;
    private int age;
	// 无参构造
	// 有参构造
	// get,set 方法
	// 此处省略
}
public class SetThread implements Runnable {
    private Student student;
    private int x = 0;

    public SetThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (student) {
                if (x % 2 == 0) {
                    student.setName("William");
                    student.setAge(30);
                } else {
                    student.setName("Andy");
                    student.setAge(5);
                }
                x++;
            }
        }
    }
}
public class GetThread implements Runnable {
    private Student student;

    public GetThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (student) {
                System.out.println(student.getName() + "---" + student.getAge());
            }
        }
    }
}
public class StudentDemo {
    public static void main(String[] args) {
        // 创建学生对象
        Student student = new Student();
        // 创建自定义线程类对象
        SetThread setThread = new SetThread(student);
        GetThread getThread = new GetThread(student);
        // 创建Thread类对象,将自定义线程类对象作为参数传递
        Thread thread = new Thread(setThread,"线程一");
        Thread thread2 = new Thread(getThread,"线程二");
        // 启动线程
        thread.start();
        thread2.start();
    }
}

生产者消费者模型线程安全问题
出现问题:
a. 相同数据出现多次()
b. 姓名和年龄不匹配(CPU执行的原子性和线程的随机性导致)

等待唤醒机制

问题:
第一次执行的时候,如果消费者先抢到 CPU 执行权,它就会去消费数据,但是此时的数据是默认值,没有任何意义,所以应该等待生产者生产完数据之后,再去消费。
如果生产者抢到 CPU 执行权,它就会生产数据,但是,如果下一次都是生产者抢到 CPU 执行权,它就会重复生产数据,这样是不合理的。应该等待消费者消费掉数据之后,再继续生产。

解决思路:
生产者:先看是否有数据,如果有,就等待,如果没有就生产数据。生产完毕之后通知消费者前来消费。
消费者:先看是否有数据,如果没有,就等待,如果有,就消费。消费完之后通知生产者生产数据。
wait():线程等待
notify():唤醒等待的线程

线程状态转换及执行流程

线程组
当项目中有许多线程需要设置一些相同的属性,比如都设置成守护线程。我们可以考虑根据线程的功能或者用途进行分组,然后针对组进行统一管理,这样的好处是方便分类操作和管理。
默认分组:

public static void method1() {
	MyThreadGroup tg = new MyThreadGroup();

	Thread thread1 = new Thread(tg);
	Thread thread2 = new Thread(tg);

    System.out.println(thread1.getThreadGroup().getName());
    System.out.println(thread2.getThreadGroup().getName());
	System.out.println(Thread.currentThread().getThreadGroup().getName());
}

设置分组:

public static void method2() {
	ThreadGroup threadGroup = new ThreadGroup("守护线程");

	MyThreadGroup tg = new MyThreadGroup();

	Thread thread1 = new Thread(threadGroup, tg);
	Thread thread2 = new Thread(threadGroup, tg);

	System.out.println(thread1.getThreadGroup().getName());
	System.out.println(thread2.getThreadGroup().getName());
	System.out.println(Thread.currentThread().getThreadGroup().getName());

	// 将这个组设置成守护线程
	threadGroup.setDaemon(true);
}

线程池
在创建线程时,成本是比较高的,因为每一次创建线程时,都要与操作系统交互。而线程池会在程序启动时,提前创建一些线程放在线程池中等待使用,这样可以大大的提高执行效率。
特点:线程执行完毕后,不会死亡,而是重新回到线程池中,成为空闲状态等下下一个线程使用。
JDK5 之前需要手动配置线程池,JDK5 之后Java开始内置线程池。
Executors:工厂类
通过下面的方法获得线程池对象:
1、
2、public static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
3、public static ExecutorService newSingleThreadExecutor():创建一个使用单个 worker 线程的 Executor
这些方法的返回值是 ExecutorService 对象,该对象表示一个线程池,它可以执行 Runnable对象 或者 Callable对象 对象代表的线程池。

操作步骤:
1、创建线程池对象
2、创建自定义类对象
3、提交 MyRunnable 到线程池
4、关闭线程池

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1 创建线程池对象
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 2 创建自定义线程类的对象
        MyThreadPool mtp = new MyThreadPool();
        // 3 提交Runnable实例
        es.submit(mtp);
        es.submit(mtp);
        // 4 关闭线程池
        es.shutdown();
    }
}

多线程第三种实现方式: Callable
Callable:这是一个接口,类似于 Runnable 接口,但是,Callable有返回值,并且可以抛出异常。
所以,如果某些线程执行完毕后需要给我们返回一个执行结果时,我们可以使用Callable接口这些方式来实现多线程。

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 30; i++) {
            System.out.println(i);
        }
        return null;
    }
}
public class MyCallableDemo {
    public static void main(String[] args) {
        // 1 创建线程池对象
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 2 创建自定义类对象
        MyCallable mc = new MyCallable();
        // 3 提交 Callable 对象
        es.submit(mc);
        es.submit(mc);
    }
}

案例:使用两个线程分别求 1-100 的和(泛型的使用)

public class MyCallable implements Callable<Integer> {
    private int num;

    public MyCallable(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i < num; i++) {
            sum += i;
        }
        return sum;
    }
}
public class MyCallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(2);
        MyCallable mc = new MyCallable(100);
        MyCallable mc2 = new MyCallable(200);

        Future<Integer> future = es.submit(mc);
        Future<Integer> future2 = es.submit(mc2);

        int num = future.get();
        int num2 = future2.get();

        System.out.println(num);
        System.out.println(num2);

        es.shutdown();
    }
}

使用匿名内部类实现多线程
匿名内部类格式:

new 类名或接口名(){
	重写方法;
};
new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };

        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 30; i++) {
                            System.out.println(Thread.currentThread().getName() + i);
                        }
                    }
                }
        ){};

        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 30; i++) {
                            System.out.println("hello" + i);
                        }
                    }
                }
            ){
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    System.out.println("world" + i);
                }
            }
        };

注:当同时重写了 Thread类 和 Runnable接口 中的 run() 方法,运行时执行的是 Thread类 中的 run() 方法。

定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台的方式执行,可以通过 Timer 和 TimerTask 类来实现定义和调度的功能。
概念:可以指定程序在某个指定的时间做某件工作。
Timer :线程的工具,用于在后台线程中安排将来执行的任务。可以将任务安排为一次性执行,或者以固定间隔重复执行。
方法:
Timer():创建一个新的定时器。
public void schedule(TimerTask task, long delay):在指定毫秒时间后执行task任务。
public void schedule(TimerTask task, long delay, long period):在指定毫秒时间后执行task任务,并在指定间隔时间后再次执行。
TimerTask :可由 Timer 一次性或重复执行的任务。
方法:
public boolean cancel():取消此定时器任务。
public abstract void run():重写run()方法,重写的内容即要执行的任务。

public class TimerDemo {
    public static void main(String[] args) {
        // 创建定时器对象
        Timer timer = new Timer();
        // 执行 Task 任务
        timer.schedule(new MyTask(timer), 3000);
    }
}
class MyTask extends TimerTask {
    private Timer timer;
    public MyTask(Timer timer) {
        this.timer = timer;
    }
    @Override
    public void run() {
        System.out.println("有内鬼,终止交易");
        timer.cancel();
    }
}

执行以上代码,可以看到三秒之后执行 run() 方法中的语句。

我是快斗,请多多指教!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值