Java自制简易线程池(不依赖concurrent包)

简介: 实时上如上文中好几次提到,java.util.concurrent包里已经帮大家实现了一个很健壮、功能强大的线程池,大家不必再去造轮子了,使用不同的BlockingQueue就可以实现不同功能的线程池。举个栗子,比如使用DelayedWorkQueue就可以实现可以定期执行的线程池了。 甚至Executors为大家封装了更为简易的线程池创建接口,但是《Alibaba Java开发手册》强制不允许使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

很久之前人们为了继续享用并行化带来的好处而不想使用进程,于是创造出了比进程更轻量级的线程。以linux为例,创建一个进程需要申请新的自己的内存空间,从父进程拷贝一些数据,所以开销是比较大的,线程(或称轻量级进程)可以和父进程共享内存空间,让创建线程的开销远小于创建进程,于是就有了现在多线程的繁荣。


但是即便创建线程的开销很小,但频繁创建删除也是很浪费性能的,于是人们又想到了线程池,线程池里的线程只会被创建一次,用完也不会销毁,而是在线程池里等待被重复利用。这种尤其适用于多而小的任务。举个极端点的例子,如果一个小任务的执行消耗不及创建和销毁一个线程的消耗,那么不使用线程池时一大半的性能消耗都会是线程创建和销毁。 最开始学java的时候,一直不理解线程池,尤其是理解不了线程是如何被复用的,以及线程池和我创建的Thread/Runnable对象有什么关系,今天我们就来写一个建议的线程池来理解这一切。(不依赖java concurrent包)


首先纠正很多人的一个误解,我们new一个Thread/Runnable对象的时候,并不是创建出一个线程,而是创建了一个需要被线程执行的任务,当我们调用Thread.start()方法的时候,jvm才会帮我们创建一个线程。线程池只是帮你执行这些任务而已,你submit的时候只是把这个任务放到某个存储里,等待线程池里空闲的线程来执行,而不是创建线程。知道了这点,所以我们首先得有个东西来存储任务,还要支持多线程下的存取,最好还支持阻塞以避免无任务时的线程空转。


除了存储外,我们还需要一些线程来消费这些任务,看到这你可能就很明白的知道了这其实是个生产者消费者模型,Java有好多种生产者消费者的实现,可以参考我之前的博客Java生产者消费者的几种实现方式。如果实现线程池,我们可以选择使用BlockingQueue来实现。虽然java concurrent包里已经实现了好多BlockingQueue,但为了让大家理解BlockingQueue做了啥,我这里用LinkedListQueue简单封装了一个简易BlockingQueue,代码如下。


package me.xindoo.concurrent;
import java.util.LinkedList;
import java.util.Queue;
public class LinkedBlockingQueue<E> {
    private Object lock;
    private Queue<E> queue;
    public LinkedBlockingQueue() {
        queue = new LinkedList<>();
        lock = new Object();
    }
    public boolean add(E e) {
        synchronized (lock) {
            queue.add(e);
            lock.notify();
            return true;
        }
    }
    public  E take() {
        synchronized (lock) {
            while (queue.isEmpty()) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return queue.poll();
        }
    }
    public synchronized int size() {
        return queue.size();
    }
}

我也只是简单在LinkedListQueue的基础上对其加了synchronized,以保证它在多线程环境下的正常运转。其次我在队列为空时通过wait()方法加了线程阻塞,以防止空队列时线程空转。既然加了阻塞也得加唤醒,每次在往队列里添加任务的时候,就会调用notify()来唤醒一个等待中的线程。


存储搞定了,我们接下来需要实现的就是消费者。消费者就是线程池里的线程,因为任务队列里的任务都是实现了Runnable接口,所以我们消费任务时都可以直接调用其run()方法来执行。当一个任务执行完成后在从队列里去取,知道整个线程池被shutdown。


package me.xindoo.concurrent;
public class ThreadPool {
    private int coreSize;
    private boolean stop = false;
    private LinkedBlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
    private Thread[] threads;
    public ThreadPool(int coreSize)  {
        this.coreSize = coreSize;
        threads = new Thread[coreSize];
        for (int i = 0; i < coreSize; i++) {
            threads[i] = new Thread(new Worker("thread"+i));
            threads[i].start();
        }
    }
    public boolean submit(Runnable command) {
        return tasks.add(command);
    }
    public void shutdown() {
        stop = true;
    }
    private class Worker implements Runnable {
        public String name;
        public Worker(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            while (!stop) {
                Runnable command = tasks.take();
                System.out.println(name + " start run, starttime " + System.currentTimeMillis()/1000);  //方便观察线程的执行情况
                command.run();
                System.out.println(name + " finished, endtime " + System.currentTimeMillis()/1000);
            }
        }
    }
}

上面就是一个线程池的实现,是不是很简单,在构造函数里初始化固定数目的线程,每个线程做的只是从队列里取任务,执行……一直循环。


没错,一个简易的线程池就通过上面几十行的代码实现了,已经可以拿去用了,甚至用在生产环境都没啥问题(后果自负,哈哈)。当然这不是一个类似于concurrent包中功能完善、各种参数可自定义的线程池,但确确实实它实现了一个线程池的基本功能——线程的复用。 接下来写个建议的测试代码,如果线程池生产者消费者模型中的消费者,那这个测试代码就是生产者,代码如下。

package me.xindoo.concurrent;
public class Test {
    private static class Task implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        ThreadPool pool = new ThreadPool(5);
        for (int i = 0; i < 30; i++) {
            pool.submit(new Task());
        }
        System.out.println("add task finished");
        try {
            Thread.sleep(10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pool.shutdown();
    }
}

执行结果如下


thread0 start run, starttime 1566724202
thread2 start run, starttime 1566724202
thread1 start run, starttime 1566724202
thread3 start run, starttime 1566724202
thread4 start run, starttime 1566724202
add task finished
thread2 finished, endtime 1566724207
thread2 start run, starttime 1566724207
thread1 finished, endtime 1566724207
thread4 finished, endtime 1566724207
thread3 finished, endtime 1566724207
thread0 finished, endtime 1566724207
thread3 start run, starttime 1566724207
thread4 start run, starttime 1566724207
thread1 start run, starttime 1566724207
thread0 start run, starttime 1566724207
thread3 finished, endtime 1566724212
thread0 finished, endtime 1566724212
thread1 finished, endtime 1566724212
thread4 finished, endtime 1566724212
thread2 finished, endtime 1566724212

测试代码也非常简单,创建一个5个线程,然后提交30个任务,从输出也可以看到的确是5个线程分批次执行完了30个任务。备注:虽然我测试代码里的任务非常简单,其实复杂的任务也是可以的。


总结

实时上如上文中好几次提到,java.util.concurrent包里已经帮大家实现了一个很健壮、功能强大的线程池,大家不必再去造轮子了,使用不同的BlockingQueue就可以实现不同功能的线程池。举个栗子,比如使用DelayedWorkQueue就可以实现可以定期执行的线程池了。 甚至Executors为大家封装了更为简易的线程池创建接口,但是《Alibaba Java开发手册》强制不允许使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。


FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

最后说点题外话,之前我们一个服务启动的时候触发了一个jdk未修复的bug https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7092821,导致线程池里所有的任务都被阻塞,但其他工作线程一直在往里提交任务,因为我们直接使用了Executors.FixedThreadPool 所以最后内存爆了… 后来我们的就结局方案就是直接使用ThreadPoolExecutor,限制了BlockingQueue的大小。

目录
相关文章
|
10天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
19天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
24 3
|
8天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
9天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1