Java 线程正确打开方式

1,152 阅读3分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

虽然关于 Java 线程的文章已经被写烂了。但是别人总结的文章只能是别人的知识点,虽然我眼睛看会了,但是脑子是记不住的。所以往后学了知识、看了文章最好还是总结下来才是自己的。

线程是操作系统调度的最小单位。

进程是操作系统分配资源的最小单位。

相信这两句话,大家应该是比较清楚的。这两句话理解也不难,就是从操作系统层面去理解线程与进程的关系。要想理解线程与进程的概念我推荐大佬的文章。

下面就总结一下 Java 中如何使用线程。

正文

线程实现

Java 中线程的实现有很多方式主要是一下两种:

  • 继承 Thread类,并重写 run方法
  • 实现 Runnable 接口的 run方法
public class ThreadDemo {
    public static void main(String[] args) {

        Thread t1 = new ExtendThread();
        t1.start();
        Thread t2 = new Thread(new ImpRunnable());
        t2.start();
    }
}

class ExtendThread extends Thread {
    @Override
    public void run(){
        System.out.println("Extends Thread success");
    }
}

class ImpRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("Implements Runnable success");
    }
}

那么为什么对于线程有两种方式去初始化,继承Thread类或者实现Runnable接口有什么区别呢?

  • Thread 类是 Java 实现Runnable接口的严格封装,因此只有当修改或扩展时,才应该继承该类
  • Runnable 接口出现更符合面向对象思想(单继承多实现),创建线程更轻量化。

总结:推荐优先使用实现Runnable接口的方式来自定义线程类。

线程池

实际上创建线程的方式还有一种是通过 CallableFuture 创建。这种方式是为了解决线程运行没有返回值的情况,主要配合线程池使用。

class ImpCallable implements Callable<Integer> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        ImpCallable task = new ImpCallable();
        // 获取任务结果
        Future<Integer> result = executor.submit(task);
        System.out.println("Task success and result is " + result.get());
    }

    @Override
    public Integer call() throws Exception {
        System.out.println("Implements Callable success");
        return 100;
    }
}

注意这里的 Executors 就是 Java 中用于创建线程池的工厂方法。返回的线程池都实现了 ExecutorService 接口。

关于线程池的初始化主要是 ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
    this.mainLock = new ReentrantLock();
    this.workers = new HashSet();
    this.termination = this.mainLock.newCondition();
    if (corePoolSize >= 0 && maximumPoolSize > 0 && maximumPoolSize >= corePoolSize && keepAliveTime >= 0L) {
        if (workQueue != null && threadFactory != null && handler != null) {
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        } else {
            throw new NullPointerException();
        }
    } else {
        throw new IllegalArgumentException();
    }
}

主要的参数解释如下:

  • corePoolSize 核心线程数
  • maximumPoolSize 最大线程数,表示在线程池中最多能创建多少个线程
  • keepAliveTime 表示线程没有任务执行时最多保持多久时间会终止
  • unit keepAliveTime 时间单位
  • workQueue 阻塞队列,用于线程池中的线程数目达到corePoolSize后,缓存任务
  • threadFactory 线程工厂,主要用来创建线程
  • handler 拒绝策略,用户设置线程池无法处理任务的规则

关于 Java 中常见的四种线程池:

  • newCachedThreadPool 可缓存线程池,可灵活回收空闲线程
  • newFixedThreadPool 定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  • newScheduledThreadPool 定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 单线程化的线程池,单线程来执行任务。保证所有任务顺序执行。

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

参考资料