搜 索

Java并发编程丨创建多线程

  • 174阅读
  • 2021年12月15日
  • 0评论
首页 / 默认分类 / 正文

前言

了解完一些基本概念之后,就开始实践吧。

实现多线程

Java 的 JDK 开发包已经自带了对并发编程技术的支持,通过它可以方便地进行多线程编程。实现多线程主要有几种方式。

方式1:继承 Thread 类

创建一个自定义的线程类 MyThread1,它继承自 Thread 类,并且重写 run() 方法,在 run() 方法中添加线程要执行的任务代码,最后通过 Thread 调用 start() 方法来启动线程。示例如下:

public class MyThread1 extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("继承 Thread 的线程运行了");
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        // 后执行
        myThread1.start();
        // 大概率先执行
        System.out.println("程序运行结束");
    }
}

多线程是异步的,线程执行的顺序未必是编辑器中代码从上到下的顺序。
执行 myThread1.start(); 这一句比较耗时,所以大概率(不是绝对)会先执行输出字符串“程序运行结束”。

方式2:实现 Runnable 接口

由于 Java 是单根继承,不支持多继承,如果线程类已经继承了其它类,就不能再继承自 Thread 类,所以需要实现 Runnable 接口来解决这种情况。
新建一个自定义的线程类 MyThread2,它已经继承自 AServer 类,再实现 Runnable 接口,也重写其中的 run() 方法。通过实现 Runnable 接口,可间接地实现“多继承”的效果。示例如下:

public class MyThread2 extends AServer implements Runnable{

    @Override
    public void saveData() {
        System.out.println("B中保存数据的方法被执行了");
    }

    @Override
    public void run() {
        System.out.println("实现 Runnable 接口的线程");
        saveData();
    }
}

public class Run {
    public static void main(String[] args) {
        // 实现 Runnable 接口
        Runnable runnable = new MyThread2();
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("程序运行结束");
    }
}

在编辑器 IDEA 中按住 Ctrl + 鼠标左键点击 Thread 类,打开其源码,可以看到它实现了 Runnable 接口。
Thread类实现了Runnable接口
这意味着 Thread 类的构造函数 Thread​(Runnable target) 不仅可以传入 Runnable 接口的对象,而且可以传入一个 Thread 类的对象,这样做可以将一个 Thread 对象中的 run() 方法交给其他线程进行调用。
Thread类的构造函数

方式3:实现 Callable 接口

无论是使用 Thread 还是 Runnable 都没有返回值,所以 JDK 1.5 推出了 Callable 接口。

  1. Callable<V> 接口位于 java.util.concurrent 包中,它更像是 Runnable 接口的增强版,call() 方法不仅有返回值还有抛出异常的功能。
  2. Future<V> 接口也位于同一个包中,它的一个实现类 FutureTask<V> 可以用来保存 call() 方法的返回值,并作为 Thread 类构造函数的 target。
  3. 使用 FutureTask<V>get() 方法获取返回值。

新建一个自定义的线程类 MyThread3,实现 Callable<V> 接口,这里设定返回值类型为 String。示例如下:

import java.util.concurrent.Callable;

public class MyThread3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "实现 Callable 接口,有返回值";
    }
}

public class Run {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 实现 Callable 接口
        MyThread3 mt3 = new MyThread3();
        FutureTask<String> ft = new FutureTask<>(mt3);
        // Thread 类接收 FutureTask 类对象
        Thread thread2 = new Thread(ft);
        thread2.start();
        // 同步方式获取返回结果
        String result = ft.get();
        System.out.println(result);
    }
}

方式4:使用线程池

在编辑器 IDEA 中使用 new Thread() 创建多线程时,它会提示:不要显式创建线程,请使用线程池。
IDEA智能提示
新建一个自定义线程类 MyThread4,实现 Callable<V> 接口,这里设定返回值类型为 String。示例如下:

public class MyThread4 implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 返回 0 - 100 的随机整数
        return "当前线程名字:" + Thread.currentThread().getName() + ",随机数:" + new Random().nextInt(100);
    }
}

为了简单点(偷懒),线程池可以这样写:

public class Run {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 4. 使用线程池创建线程
        ExecutorService es = Executors.newFixedThreadPool(10);
        // 结果集
        List<Future<String>> resultList = new ArrayList<>();

        // 创建 20 个任务并执行
        for (int i = 0; i < 20; i++) {
            // 使用 ExecutorService 执行 Callable 类型的任务,并将结果保存在 task 变量中
            Future<String> task = es.submit(new MyThread4());
            // 将任务执行结果存储到 List 中
            resultList.add(task);
        }
        es.shutdown();

        // 遍历结果集
        for (int i = 0; i < resultList.size(); i++){
            Future<String> result = resultList.get(i);
            // 打印各个线程(任务)执行的结果
            System.out.println(result.get());
        }
    }

但是,阿里巴巴的 Java 开发手册中,禁止使用 Executors 创建线程池,虽然 Executors 极大的简化了我们创建各种类型线程池的方式。
强制禁止
所以,最好使用 ThreadPoolExecutor 创建线程:

public class Run {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 5. 使用 ThreadPoolExecutor 创建线程池
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数
        int maxnumPoolSize = 10;
        // 指非核心线程的空闲时间达到一定的阈值会被回收
        long keepAliveTime = 2;
        // 以秒为单位
        TimeUnit timeUnit = TimeUnit.SECONDS;
        // 工作队列,存放提交的等待执行任务
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
        ThreadPoolExecutor service = null;
        try {
            // 创建线程池
            service = new ThreadPoolExecutor(corePoolSize, maxnumPoolSize, keepAliveTime,
                    timeUnit, workQueue, new ThreadPoolExecutor.AbortPolicy());
            // 循环提交任务
            for (int i = 0; i < 20; i++){
                Future<String> task = service.submit(new MyThread4());
                // 打印输出结果
                System.out.println(task.get());
                // 线程休眠 1 S,再提交下一个任务,保证顺序
                Thread.sleep(1000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            service.shutdown();
        }
    }
}

分析

在编辑器 IDEA 中,打开 ThreadPoolExecutor 的源码,可以看到其构造方法中使用了 Executors.defaultThreadFactory()
静态方法defaultThreadFactory
继续点击溯源,如下图所示,可以看到这个静态方法 defaultThreadFactory() 返回了一个 ThreadFactory 类型的 DefaultThreadFactory 实例。
返回DefaultThreadFactory
继续点击溯源,如下图所示,可以看到这个私有静态类 DefaultThreadFactory 实现了 ThreadFactory 接口,其中它设置了一些默认值:线程池的序号 poolNumber 设置为 1,线程的序号 threadNumber 设置为 1。

  • 在其构造函数中,设置线程所属的线程组 group,设置线程的名字前缀 namePrefix
  • 在其 newThread(Runnable r) 函数中,根据其私有属性:groupnamePrefixthreadNumber 创建了一个新线程,然后再设置该线程不是守护线程、该线程的优先级为普通等级。

DefaultThreadFactory源码
那么,可以说:虽然线程工厂设置了很多属性,但最终还是通过 new Thread() 来创建线程的,只不过构造函数传入的参数比较多。
无论表面上封装了多少层,只要本质不变,打开封装后,就会知道最终还是继承 Thread 类或实现 Runnable 接口得到的。

参考

Java 全栈知识体系
打 赏
  • 支付宝
  • 微信
Alipay
WeChatPay
评论区
暂无评论
avatar