关于线程

  • 一个 Java 应用程序 java.exe ,其实至少有三个线程: main() 主线程、 gc() 垃圾回收线程和异常处理线程;
  • Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类来体现;
  • 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体;
  • 线程是通过调用具体 Thread 对象的 start() 方法来启动的,而非直接调用 run() 方法。

Thread 类

1、Thread 类的声明

Thread 类实现了 Runnable 接口

  1. public class Thread implements Runnable

2、Thread 类的有关方法

void start()
启动线程,并执行对象的run()方法

run()
线程在被调度时执行的操作

String getName()
返回线程的名称

void setName(String )
设置线程的名称

this.yield() / Thread.currentThread().yield()
释放当前线程的CPU执行权

线程实例对象.join()
在线程A中调用线程B的join()方法,此时线程A就进入阻塞状态。直到线程B完全执行完以后,线程A才结束阻塞状态

stop()
已过时。当执行此方法时,强制结束当前线程。

sleep(long millitime)
让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程一直是阻塞状态。

isAlive()
判断当前线程是否存活

如何创建一个空线程

我在 Spring Boot 的单元测试中写了一个测试样例

  1. import org.junit.jupiter.api.Disabled;
  2. import org.junit.jupiter.api.DisplayName;
  3. import org.junit.jupiter.api.Test;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. @Disabled
  7. @DisplayName("创建空线程")
  8. public class MutiThreadTests {
  9. private static final Logger logger = LoggerFactory.getLogger(MutiThreadTests.class);
  10. @Test
  11. @DisplayName("基本测试")
  12. public void createEmptyThread() {
  13. Thread thread = new Thread();
  14. logger.info("线程名称:{}", thread.getName());
  15. logger.info("线程 ID:{}", thread.getId());
  16. logger.info("线程状态:{}", thread.getState());
  17. logger.info("线程优先级:{}", thread.getPriority());
  18. }
  19. }

输出

13:32:15.906 [main] INFO com.sugarless.sbapiservice.JavaTests.NO4_多线程.创建线程.NO0_创建空线程.MutiThreadTests - 线程名称:Thread-1
13:32:15.910 [main] INFO com.sugarless.sbapiservice.JavaTests.NO4_多线程.创建线程.NO0_创建空线程.MutiThreadTests - 线程 ID:21
13:32:15.910 [main] INFO com.sugarless.sbapiservice.JavaTests.NO4_多线程.创建线程.NO0_创建空线程.MutiThreadTests - 线程状态:NEW
13:32:15.910 [main] INFO com.sugarless.sbapiservice.JavaTests.NO4_多线程.创建线程.NO0_创建空线程.MutiThreadTests - 线程优先级:5

方法一:继承 Thread 类创建线程类

通过上面空线程的例子可以看出,新线程如果需要并发执行自己的代码,需要做以下两件事情:
(1)需要继承 Thread 类,创建一个新的线程类。
(2)同时重写 run() 方法,将需要并发执行的业务代码编写在 run() 方法中。

步骤:
1)创建一个类继承于 Thread 类
2)重写 Thread 类的 run() 方法
3)创建子类对象
4)通过子类对象调用 start()

public class Main {
    public static void main(String[] args) {
        Data data = new Data();

        Counter thread1 = new Counter(data);
        Counter thread2 = new Counter(data);

        thread1.setName("Tom");
        thread2.setName("Jerry");

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

// 多个线程共享数据
class Data {
    public int count;
}

class Counter extends Thread {
    public Counter(Data data) {
        this.data = data;
    }

    private Data data;

    @Override
    public void run() {
        //遍历100以内的所有正整数,输出偶数
        for (int i = 0; i < 100000; i++) {
            if (i % 2 == 0) {
                System.out.println(this.getName() + ":" + ++data.count);
            }
        }
    }
}

方法二:实现 Runnable 接口创建线程目标类

将需要异步执行的业务逻辑代码放在 Runnable 实现类的 run() 方法中,将 Runnable 实例作为 target 执行目标传入 Thread 实例。

步骤:
(1)定义一个新类实现 Runnable 接口。
(2)实现 Runnable 接口中的 run() 抽象方法,将线程代码逻辑存放在该 run() 实现版本中。
(3)通过 Thread 类创建线程对象,将 Runnable 实例作为实际参数传递给 Thread 类的构造器,由 Thread 构造器将该 Runnable 实例赋值给自己的 target 执行目标属性。
(4)调用 Thread 实例的 start() 方法启动线程。
(5)线程启动之后,线程的 run() 方法将被 JVM 执行,该 run() 方法将调用 target 属性的 run() 方法,从而完成 Runnable 实现类中业务代码逻辑的并发执行。

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

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

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

class Counter implements Runnable {
    public int count;

    @Override
    public void run() {
        String name = Thread.currentThread().getName();

        for (int i = 0; i < 100000; i++) {
            if (i % 2 == 0) {
                System.out.println(name + ": " + ++count);
            }
        }
    }
}

值得注意的是,run() 方法实现版本中在获取当前线程的名称时,所用的方法是在外部类 ThreadUtil 中定义的 getCurThreadName() 静态方法,而不是 Thread 类的 getName() 实例方法。原因是:这个 RunTarget 内部类和 Thread 类不再是继承关系,无法直接调用 Thread 类的任何实例方法。

通过实现 Runnable 接口的方式创建的执行目标类,如果需要访问线 程的任何属性和方法,必须通过 Thread.currentThread() 获取当前的线程 对象,通过当前线程对象间接访问。

通过继承 Thread 类的方式创建的线程类,可以在子类中直接调用 Thread 父类的方法访问当前线程的名称、状态等信息。这也是使用 Runnable 实现异步执行与继承 Thread 方法实现异步执行不同的地方。

优雅创建 Runnable 线程目标类的两种方式

使用 Runnable 创建线程目标类除了直接实现 Runnable 接口之外,还有两种比较优雅的代码组织方式:
(1)通过匿名类优雅地创建 Runnable 线程目标类。
(2)使用 Lambda 表达式优雅地创建 Runnable 线程目标类。

1)通过匿名类优雅地创建Runnable线程目标类

在实现 Runnable 编写 target 执行目标类时,如果 target 实现类是一次性类,可以使用匿名实例的形式。

样例代码:

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@Disabled
@DisplayName("通过匿名类创建 Runnable 线程目标类")
public class MutiThreadTests {
    @Test
    public void test() {
        int threadCount = 10;
        int threadNo = 1;

        Thread thread = null;

        //使用Runnable的匿名类创建和启动线程
        for (int i = 0; i < 2; i++) {
            thread = new Thread(new Runnable() { //① 匿名实例
                @Override
                public void run() { //② 异步执行的业务逻辑
                    for (int j = 1; j < threadCount; j++) {
                        System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);
                    }
                    System.out.println(Thread.currentThread().getName() + " 运行结束.");
                }
            }, "RunnableThread" + threadNo++);
            thread.start();
        }

        System.out.println(Thread.currentThread().getName() + " 运行结束.");
    }
}

2)使用Lambda表达式优雅地创建Runnable线程目标类

Runnable 接口是一个函数式接口,在接口实现时可以使用 Lambda 表 达式提供匿名实现,编写出比较优雅的代码。

样例代码:

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@Disabled
@DisplayName("使用 Lambda 表达式创建 Runnable 线程目标类")
public class MutiThreadTests {
    @Test
    public void test() {
        int threadCount = 10;
        int threadNo = 1;

        Thread thread = null;
        //使用Lambda表达式形式创建和启动线程
        for (int i = 0; i < 2; i++) {
            thread = new Thread(() -> { //①Lambda表达式
                for (int j = 1; j < threadCount; j++) {
                    System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);
                }

                System.out.println(Thread.currentThread().getName() + " 运行结束.");
            }, "RunnableThread" + threadNo++);

            thread.start();
        }

        System.out.println(Thread.currentThread().getName() + " 运行结束.");
    }
}

先回顾一下Runnable接口,其源代码中有一个小玄机,具体如下:

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

源代码中的小玄机为:在 Runnable 接口上声明了一个 @FunctionalInterface 注解。该注解的作用是:标记 Runnable 接口是一 个“函数式接口”。在 Java 中,“函数式接口”是有且仅有一个抽象方法的 接口。反过来说,如果一个接口中包含两个或两个以上的抽象方法,就 不能使用 @FunctionalInterface 注解,否则编译会报错。

@FunctionalInterface 注解不是必需的,只要一个接口符合“函数式接 口”的定义,使用时加不加 @FunctionalInterface 注解都没有影响,都可以 当作“函数式接口”来使用。

创建 Lambda 表达式版本的 target 执行目标实例的代码与创建 target 执 行目标匿名实例的代码的区别也很小,区别主要在代码①处,其他的部 分完全相同。在代码①处,通过 Lambda 表达式直接编写 Runnable 接口的 run() 方法的实现代码,接口的名称(Runnable)、方法的名称 run()统统 都被省略,仅剩下了 run() 方法的形参列表和方法体。

总体而言,经过对比可以发现:使用 Lambda 表达式创建 target 执行 目标实例,代码已经做到了极致的简化。

通过实现 Runnable 接口的方式创建线程目标类的

优缺点

1)缺点:

(1)所创建的类并不是线程类,而是线程的 target 执行目标类,需 要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
(2)如果访问当前线程的属性(甚至控制当前线程),不能直接 访问 Thread的 实例方法,必须通过 Thread.currentThread() 获取当前线程 实例,才能访问和控制当前线程。

2)优点:

(1)可以避免由于 Java 单继承带来的局限性。如果异步逻辑所在 类已经继承了一个基类,就没有办法再继承 Thread 类。比如,当一个 Dog 类继承了 Pet 类,再要继承 Thread 类就不行了。所以在已经存在继承 关系的情况下,只能使用实现 Runnable 接口的方式。
(2)逻辑和数据更好分离。通过实现 Runnable 接口的方法创建多 线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资 源被多个线程逻辑异步、并行处理的场景中,通过实现 Runnable 接口的 方式设计多个 target 执行目标类可以更加方便、清晰地将执行逻辑和数 据存储分离,更好地体现了面向对象的设计思想。

总之,在大多数情况下,偏向于通过实现Runnable接口来实现线程 执行目标类,这样能使代码更加简洁明了。后面介绍线程池的时候会讲 到,异步执行任务在大多数情况下是通过线程池去提交的,而很少通过 创建一个新的线程去提交,所以更多的做法是,通过实现 Runnable 接口 创建异步执行任务,而不是继承 Thread 去创建异步执行任务。

方法三:使用 Callable 和 FutureTask 创建

线程 通过 FutureTask 类和 Callable 接口的联合使用可以创建能够获取异步 执行结果的线程。

样例代码:

public class Main {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();

        FutureTask futureTask = new FutureTask(numThread);

        Thread thread = new Thread(futureTask);

        thread.start();

        try {
            Object sum = futureTask.get();
            System.out.println("总和为:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumThread implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

前面已经介绍了继承 Thread 类或者实现 Runnable 接口这两种方式来 创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的 结果。

这是一个比较大的问题,很多场景都需要获取异步执行的结果,通 过 Runnable 无法实现,是因为它的 run() 方法不支持返回值。 为了解决异步执行的结果问题,Java 语言在 1.5 版本之后提供了一种 新的多线程创建方法:通过 Callable 接口和 FutureTask 类相结合创建线 程。

具体步骤如下:
1)创建一个 Callable 接口的实现类,并实现其 call() 方法,编写好 异步执行的具体逻辑,可以有返回值。
2)使用 Callable 实现类的实例构造一个 FutureTask 实例。
3)使用 FutureTask 实例作为 Thread 构造器的 target 入参,构造新的 Thread 线程实例。

与使用Runnable相比,Callable功能更强大些。
1)相比run方法,可以有返回值
2)方法可以抛出异常
3)支持泛型的返回值
4)需要借助FutureTask类,比如获取返回结果

方法四:通过线程池创建线程(开发中最常用)

前面的示例中,所创建的 Thread 实例在执行完成之后都销毁了,这 些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资 源耗费上都很高(稍后会介绍),在高并发的场景中,断然不能频繁进 行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复 用,这就涉及线程池的技术。Java 中提供了一个静态工厂来创建不同的 线程池,该静态工厂为 Executors 工厂类。

样例代码:

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.concurrent.*;

@Disabled
@DisplayName("通过线程池创建")
public class MutiThreadTests {
    public static final int MAX_TURN = 5;
    public static final int COMPUTE_TIMES = 100000000;

    //创建一个包含三个线程的线程池
    private static ExecutorService pool = Executors.newFixedThreadPool(3);

    static class DemoThread implements Runnable {
        @Override
        public void run() {
            for (int j = 1; j < MAX_TURN; j++) {
                System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);

                // sleep
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class ReturnableTask implements Callable<Long> {
        //返回并发执行的时间
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " 线程运行开始.");

            for (int j = 1; j < MAX_TURN; j++) {
                System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);
                Thread.sleep(10);
            }

            long used = System.currentTimeMillis() - startTime;
            System.out.println(Thread.currentThread().getName() + " 线程运行结束.");
            return used;
        }
    }

    @Test
    @DisplayName("测试")
    public void test() throws ExecutionException, InterruptedException {
        pool.execute(new DemoThread());// 执行线程实例,无返回

        pool.execute(new Runnable() {
            @Override
            public void run() {
                for (int j = 1; j < MAX_TURN; j++) {
                    System.out.println(Thread.currentThread().getName() + ", 轮次:" + j);

                    // sleep
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        //提交Callable 执行目标实例,有返回
        Future<Long> future = pool.submit(new ReturnableTask());
        Long result = future.get();
        System.out.println("异步任务的执行结果为:" + result);

//        Thread.sleep(Integer.MAX_VALUE);
    }
}

但在生产环境中,线程池最好不要使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

ExecutorService线程池的execute(…)与submit(…)方法的区别如下。

(1)接收的参数不一样 submit() 可以接收两种入参:无返回值的 Runnable 类型的 target 执行 目标实例和有返回值的 Callable 类型的 target 执行目标实例。而 execute() 仅仅接收无返回值的 target 执行目标实例,或者无返回值的 Thread 实例。

(2)submit() 有返回值,而 execute() 没有 submit() 方法在提交异步 target 执行目标之后会返回 Future 异步任务 实例,以便对 target 的异步执行过程进行控制,比如取消执行、获取结 果等。 execute() 没有任何返回, target 执行目标实例在执行之后没有办法 对其异步执行过程进行控制,只能任其执行,直到其执行结束。

背景:
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具。

好处:
1)提高响应速度(减少了创建新线程的时间)
2)降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3)便于线程管理
CorePoolSize 核心池的大小
MaximumPoolSize 最大线程数
KeepAliveTime 线程没有任务时最多保持多长时间后会终止

步骤:
1、提供指定线程数量的线程池
2、执行指定的线程的操作。需要提供实现runnable接口或callable接口实现类的对象
3、关闭连接池

public class Main {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);

//        System.out.println(service.getClass());//ThreadPoolExecutor
        //设置线程池的参数
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        service1.setCorePoolSize();
//        service1.setKeepAliveTime();
//        service1.setMaximumPoolSize();
//        service1.setThreadFactory();
//        service1.setRejectedExecutionHandler();

        service.execute(new NumberThread());//适合使用于Runnable
        service.execute(new NumberThread1());//适合使用于Runnable
//        service.submit();//适合适用于Callable
        service.shutdown();
    }
}

class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

为什么启动线程要调用 start() 而不是 run() ?

run() 方法只是一个普通的方法,如果主线程中直接调用 Thread 对象的 run() 方法,Thread.currentThread().getName() 获取到的线程名将会是主线程的名称,而不是具体线程的。也就是说,执行过程将会变为串行化。

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.setName("Tom");
        thread2.setName("Jerry");

        thread1.run();
        thread2.run();

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

class MyThread extends Thread {
    @Override
    public void run() {
        String name = Thread.currentThread().getName();

        for (int i = 0; i < 50; i++) {
            System.out.println(name + ": " + i);
        }
    }
}

看一下 Thread.start 的源码

public synchronized void start() {
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

其中 start0 方法是本地方法,由 JVM 调用,其底层是 C/C++ 实现。真正实现多线程的效果,其实是 start0 这个方法。

private native void start0();

start 方法调用 start0 方法后,该线程并不一定会立马执行,而是将线程变成了可运行状态。具体什么时候执行,取决于 CPU ,由 CPU 统一调度。
image.png

多线程售票问题

高并发场景下,库存超卖。

public class Main {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        TicketingWindow window1 = new TicketingWindow(ticket);
        TicketingWindow window2 = new TicketingWindow(ticket);
        TicketingWindow window3 = new TicketingWindow(ticket);

        window1.setName("1号售票窗口");
        window2.setName("2号售票窗口");
        window3.setName("3号售票窗口");

        window1.start();
        window2.start();
        window3.start();
    }
}

class Ticket {
    public int stock = 100;
}

class TicketingWindow extends Thread {
    public TicketingWindow(Ticket ticket) {
        this.ticket = ticket;
    }

    private Ticket ticket;

    @Override
    public void run() {
        while (ticket.stock > 0) {
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName() + ":" + --ticket.stock);
        }

        System.out.println(this.getName() + "停止服务");
    }
}