java线程池和多线程的使用详解

Java 多线程和线程池使用

java多线程实现的几种方法

1.继承Thread类

继承Thread类,重写run方法,创建线程类对象调用start方法启动线程。

public class ThreadDemo {

    /**
     * 继承Thread类创建线程
     */
    public static class MyThread extends Thread {

        public MyThread() {
        }

        public MyThread(String threadName) {
            this.setName(threadName);
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("继承Thread类创建线程:" + this.getName() + " " + i);
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread("thread1");
        MyThread myThread2 = new MyThread("thread2");
        MyThread myThread3 = new MyThread("thread3");
        myThread1.start();
        myThread2.start();
        myThread3.start();
    }

}

程序三次运行结果(部分):

第一次 第二次 第三次

从以上运行结果可以看出线程的执行顺序是随机性的和代码的编写顺序无关。

2.实现Runnable接口

Runnable接口中只有一个抽象run方法。

Runnable接口

实现Runnable接口重写run方法创建自己的线程,调用start方法启动线程。

public class ThreadDemo {

    /**
     * 实现Runnable创建线程
     */
    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("实现Runnable创建线程:" + Thread.currentThread().getName() + " " + i);
            }
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable);
        Thread thread2 = new Thread(myRunnable);
        Thread thread3 = new Thread(myRunnable);
        thread1.setName("thread1");
        thread2.setName("thread2");
        thread3.setName("thread3");
        thread1.start();
        thread2.start();
        thread3.start();
    }

}

利用构造函数Thread(Runnable target) 启动线程,实际上Thread类也实现了Runnable接口,这样就可以将Thread对象交由其他线程调用run方法执行。

3.实现Callable接口

Callable接口

Runnable是出自jdk1.0,Callable出自jdk1.5,Callable相当于是对Runnable接口的增强,Runnable和Callable接口的区别就在于Callable接口有一个带有返回值的call方法,并且可以抛出异常

public class ThreadDemo {

    /**
     * 实现Callable创建线程
     */
    public static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            for (int i = 0; i < 10; i++) {
                System.out.println("实现Callable创建线程:" + Thread.currentThread().getName() + " " + i);
            }
            return "线程执行完成";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        long start = System.currentTimeMillis();
        thread.start();
        String result = futureTask.get();
        System.out.printf("线程执行结果【%s】,耗时:%d ms", result, System.currentTimeMillis() - start);
    }

}

实现Callable接口,重写call方法,通过FutureTask交给线程执行,阻塞等待结果返回。

callable逻辑交由异步线程处理,主线程通过阻塞接受异步线程返回内容。

以下是根据JAVA FutureTask类源码注释翻译的内容:

FutureTask:一个可取消的异步的计算,这个类实现了runnable和Future接口。future提供了开始,取消计算,查询计算是否完成。get()方法将会是堵塞的,调用get()方法将会堵塞直到计算完成返回结果。一旦计算完成了就不可以再次开始或者取消,除非是调用runAndReset()方法。

FutureTask有NEW,COMPLETING,NORMAL,EXCEPTIONAL,CANCELLED,INTERRUPTINGINTERRUPTED七种状态,创建时状态为NEW,结果未赋值时更新为COMPLETING,结果赋值后更新为NORMAL,发生异常后会将状态置为EXCEPTIONAL,调用cancel方法后更新为CANCELLED或者INTERRUPTING状态。

Java线程池的使用

线程池的优点

  1. 线程池能够最大化资源的利用率,因为它可以重复利用已经创建的线程,避免了每次创建和销毁线程时的开销。

  2. 线程池可以根据需要动态地创建和销毁线程,从而更好地管理和调度线程,提高程序的性能和响应速度。

  3. 线程池可以更有效地处理大量并发请求,因为它可以将任务拆分成较小的部分,并发地提交给线程池中的线程处理,从而更快地完成任务。

  4. 线程池可以减少锁竞争等线程间相互干扰的问题,提高了程序的正确性和可靠性。

  5. 线程池可以提高程序的安全性,因为它可以限制同一时间只有一个线程可以执行某个任务,减少了多个线程同时执行同一个任务时可能发生的错误。

池化技术基本都具有提高程序的性能、响应速度、资源利用率、方便统一管理的优点

Java线程池

ThreadPoolExecutor 线程池位于 java.util.concurrent 包下,是 Java 中用于实现线程池的一种基础类。

Executor接口继承关系

Executor接口继承关系

  • Executor线程池相关顶级接口,它将任务的提交与任务的执行分离开来。

  • ExecutorService继承并扩展了Executor接口,提供了Runnable、FutureTask等主要线程实现接口扩展。

  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。

  • ScheduledExecutorService接口,是延时执行类任务的主要实现。

ThreadPoolExecutor构造方法参数详解

以下是ThreadPoolExecutor包含所有参数的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize:线程池中核心线程的数量。

    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。

  • maximumPoolSize:线程池中最大线程数量。

    当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

  • keepAliveTime:线程池中空闲线程的存活时间。

    一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么超过keepAliveTime设定的时间后,这个空闲线程会被销毁。

  • unit:空闲线程的存活时间单位。

    java.util.concurrent.TimeUnit 枚举类。

  • workQueue:工作队列。

    存放工作任务的队列,任务调度时会从该队列取出任务。JDK提供了四种实现队列:

    1. ArrayBlockingQueue

    其是一个基于数组的阻塞队列,底层使用数组进行元素的存储。创建该阻塞队列实例需要指定队列容量,故其是一个有界队列。在并发控制层面,无论是入队还是出队操作,均使用同一个ReentrantLock可重入锁进行控制,换言之生产者线程与消费者线程间无法同时操作。

    1. LinkedBlockingQuene

    其是一个基于链表的阻塞队列,底层使用链表进行元素的存储。该阻塞队列容量默认为 Integer.MAX_VALUE,即如果未显式设置队列容量时可以视为是一个无界队列;反之构建实例过程中指定队列容量,则其就是一个有界队列。在并发控制层面,其使用了两个ReentrantLock可重入锁来分别控制对入队、出队这两种类型的操作。使得生产者线程与消费者线程间可以同时操作提高效率。

    1. SynchronousQuene

    其是一个同步队列。特别地是由于该队列没有容量无法存储元素,故生产者添加的数据会直接被消费者获取并且立刻消费。所以当生产者线程添加数据时,如果此时恰好有一个消费者已经准备好获取队头元素了,则会添加成功;否则要么添加失败返回false要么被阻塞。如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

    1. PriorityBlockingQueue

    线程安全版本的优先级队列PriorityBlockingQueue,其是一个支持优先级的无界阻塞队列。底层使用数组实现元素的存储、最小堆的表示。默认使用元素的自然排序,即要求元素实现Comparable接口;或者显式指定比较器Comparator。在并发控制层面,无论是入队还是出队操作,均使用同一个ReentrantLock可重入锁进行控制。值得一提的是,在创建该队列实例时虽然可以指定容量。但这并不是队列的最终容量,而只是该队列实例的初始容量。一旦后续过程队列容量不足,其会自动进行扩容。值得一提的是,为了保证同时只有一个线程进行扩容,其内部是通过CAS方式来实现的,而不是利用ReentrantLock可重入锁来控制。故PriorityBlockingQueue是一个无界队列。

    以上队列相关描述取自: Java多线程之阻塞队列(知乎文章)

  • threadFactory:线程工厂。

    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程。

  • handler:拒绝策略。

    当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就会执行相应的拒绝策略。jdk中提供了4中拒绝策略:

    1. CallerRunsPolicy

    只要线程池没有关闭,就交由调用方线程运行。谁提交任务谁来执行这个任务,即将任务执行放在提交的线程里面,减缓了线程的提交速度,相当于负反馈。在提交任务线程执行任务期间,线程池又可以执行完部分任务,从而腾出空间来。

    使用场景:一般不允许失败的、对性能要求不高、并发量较小的场景下使用。

    1. AbortPolicy

    直接丢弃任务,抛出RejectedExecutionException异常。

    1. DiscardPolicy

    直接丢弃任务,什么都不做。

    1. DiscardOldestPolicy

    弃任务队列中等待事件最长的,即最老的任务。

线程和线程池的使用规范

java提供了几种常见的线程池创建方式:

  • FixThreadPool 可重用固定线程池

    线程池的大小一旦达到设定数量就会保持不变。

  • SingleThreadExcutor 单线程化的线程池

    只有一个线程的线程池,任务按照提交的次序顺序执行的。

  • CachedThreadPool 可缓存线程池

    此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。线程池的线程数可达到Integer.MAX_VALUE,即2147483647。

具体实现可自行查看JDK源码。

注意

  1. 对于线程资源应通过线程池提供,避免自行显示创建线程。

  2. 虽然java提供了以上三种常见的线程池创建方式,但是以上三种队列,队列长度或者线程数的最大限制可达到Integer.MAX_VALUE,可能导致内存溢出,所以线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor的方式。

最新版阿里巴巴泰山版《Java开发手册》对线程和线程池的使用也提出了强制类型要求,如下图:

阿里java开发手册

posted @ 2023-05-11 11:04  程序员的世外桃源  阅读(160)  评论(0编辑  收藏  举报