Java多线程与并发编程 | 线程的创建方式

380 阅读3分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

前言

面试中在涉及到多线程这一块的时候,或多或少都被问到过线程的创建方式,如果平时不留意,一时真不知道该怎么回答.借这次学习机会,梳理一下。

创建线程的三种方式

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 使用CallableFuture创建线程

继承Thread

public class ThreadDemo_1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread.start();
        myThread2.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

打印输出

Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:0
Thread-1:3
Thread-0:1
Thread-1:4
Thread-0:2
Thread-0:3
Thread-0:4

可以看到两个线程都完成了打印任务,并且交替打印互不影响

实现Runnable

public class ThreadDemo_2 {
    public static void main(String[] args) {
        MyThread02 myThread = new MyThread02();
        Thread thread01 = new Thread(myThread);
        Thread thread02 = new Thread(new MyThread02());
        thread01.start();
        thread02.start();
    }
}

class MyThread02 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

打印输出

Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-1:0
Thread-0:4
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4

这里要注意的是在实现Runnable方法里是不能通过this.getName()获取当前线程的名称,通过Thread.currentThread().getName()去获取

使用Callable和Future

  • JDK1.5以后为我们专门提供了一个并发工具包java.util.concurrent.
  • JUC包下包含许多线程安全、测试良好、高性能的并发结构模块。创建concurrent的目的就是要实现Collection框架对数据结构所执行的并发操。通过提供一组可靠的,高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。简单说GUC专为并发而生的。
public class ThreadDemo_3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建一个线程池。里面默认有3个空线程。
        // Executors是调度器,对线程池进行管理
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        MyThread03 myThread01 = new MyThread03();
        MyThread03 myThread02 = new MyThread03();

        // 将这个对象扔到线程池里,线程池会自动分给一个线程来运行这个对象的call方法
        // Future用于接收线程内部call方法的返回值
        Future<Integer> submit = executorService.submit(myThread01);
        Future<Integer> submit2 = executorService.submit(myThread02);

        // 关闭线程池释放所有资源
        executorService.shutdown();
        System.out.println("submit all返回值:" + submit.get());
        System.out.println("submit2 call返回值:" + submit2.get());
    }
}

class MyThread03 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int count = 0;
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            count++;
        }
        return count;
    }
}

打印输出

pool-1-thread-2:0
pool-1-thread-1:0
pool-1-thread-2:1
pool-1-thread-1:1
pool-1-thread-2:2
pool-1-thread-1:2
pool-1-thread-2:3
pool-1-thread-2:4
pool-1-thread-1:3
pool-1-thread-1:4
submit all返回值:5
submit2 call返回值:5

从打印可以看出,这里引用了线程池的概念,线程池中的线程通过接收到发布的任务,然后去完成对应的事情。

三种方式优缺点

继承Thread

  • 优点: 编程简单,执行效率高
  • 缺点: 单继承,无法对线程组有效控制
  • 使用背景: 不推荐使用 实现Runnable
  • 优点: 面向接口编程,执行效率高
  • 缺点: 无法对线程组有效控制,没有返回值、异常
  • 使用背景: 简单的多线程程序 利用线程池
  • 优点: 容器管理线程,允许返回值与异常
  • 缺点: 执行效率相对低,编程麻烦
  • 使用背景: 企业级应用 推荐使用

总结

实现Runnable和Callable的区别

  • 实现了Runnable接口后无法返回结果信息,实现了Callable接口后有返回值。
  • 实现了Runnable接口异常无法通过throws抛出异常,实现了Runnable接口后可以直接抛出Exception异常