Java编程入门指南

发表时间: 2024-05-14 07:37

在日常开发过程中,为了提高程序的运行效率,往往会采用使用多线程。Java实现多线程主要有以下几种方式:

  1. 继承Thread类创建一个新的类继承自Thread类,并重写其run()方法,在run()方法中定义需要并行执行的代码。然后创建这个自定义类的实例,并调用其start()方法来启动线程。这种方式的缺点是Java不支持多重继承,因此如果自定义类已经继承了其他类,则不能采用此方式。
  2. 实现Runnable接口: 创建一个类实现Runnable接口,并实现其run()方法。然后将此类的实例作为参数传递给Thread类的构造器,创建Thread对象,并通过调用该对象的start()方法来启动线程。这种方式因为基于接口实现,所以更加灵活,允许类继承其他类。
  3. 实现Callable接口和使用FutureTask: 与Runnable类似,但Callable接口提供了一个call()方法,该方法可以有返回值并且可以抛出异常。要将Callable任务转换为线程,需要将其包装进一个FutureTask对象,然后将FutureTask作为参数传递给Thread对象,或者直接提交给ExecutorService来管理执行。FutureTask还提供了检查任务执行状态和获取返回值的方法。
  4. 使用线程池(ExecutorService): 通过Executors类提供的工厂方法创建不同类型的线程池,如固定大小的线程池newFixedThreadPool、单线程的线程池newSingleThreadExecutor、可缓存线程池newCachedThreadPool等。线程池可以管理线程的生命周期,提高线程复用率,控制最大并发数,有效提高程序性能。提交任务到线程池使用execute(Runnable)或submit(Runnable/Callable)方法。

下面聊下在工作中使用线程池遇到的一个问题,项目中有一个线程工具类,大概如下:

public class ThreadUtil {    /**     * 核心线程数     */    private static final int corePoolSize = 5;    /**     * 最大线程数     */    private static final int maximumPoolSize = 10;    /**     * 线程存活时间     */    private static final int keepAliveTime = 2000;    private static final ThreadPoolExecutor pool;    static {        pool = new ThreadPoolExecutor(corePoolSize,                maximumPoolSize,                keepAliveTime,                TimeUnit.SECONDS,                new LinkedBlockingQueue<Runnable>(200),                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());    }    public static ThreadPoolExecutor getPool(){        return pool;    }}

该类在初始化时会创建一下线程池,并提供静态方法返回线程池。本人知道有这么一个类存在,但是一直没有使用过。最近一个同事在使用这个类时遇到了一个问题,大概代码如下:

public void test02(){        CountDownLatch countDownLatch = new CountDownLatch(50);        ThreadPoolExecutor pool = ThreadUtil.getPool();        for (int i = 0; i < 5; i++) {            pool.submit(()->{                try {                    //模拟操作,等待5s                    Thread.sleep(5000L);                } catch (InterruptedException e) {                    log.error("线程中断", e);                }                countDownLatch.countDown();            });        }        try {            boolean await = countDownLatch.await(30L, TimeUnit.SECONDS);            if(!await){                log.error("任务执行超时");            }        } catch (InterruptedException e) {            log.error("线程中断", e);        }    }

该方法在执行时,有时成功,有时却提示“任务执行超时”。我看下了代码比较简单,没有明显的问题。思考了一会,线程里的方法并不复杂,不会执行耗时不会很长。那么问题应该出现在线程池本身上,这里线程池使用的公共方法获取的,那么应该是同时有其他任务用到了线程池,导致新的任务还没来的及超时。使用开发工具看了下,确实有好几处代码也使用了该线程池。为了验证问题,对代码临时修改下,使用
java.util.concurrent.Executors#newFixedThreadPool(int):

public void test02(){    CountDownLatch countDownLatch = new CountDownLatch(50);    ExecutorService pool = Executors.newFixedThreadPool(5);    for (int i = 0; i < 5; i++) {        pool.submit(()->{            try {                //模拟操作,等待5s                Thread.sleep(5000L);            } catch (InterruptedException e) {                log.error("线程中断", e);            }            countDownLatch.countDown();        });    }    pool.shutdown();    try {        boolean await = countDownLatch.await(30L, TimeUnit.SECONDS);        if(!await){            log.error("任务执行超时");        }    } catch (InterruptedException e) {        log.error("线程中断", e);    }}

经过测试,问题不再复现,得到解决。在此大概总结下线程池知识进行简单总结。

Java线程池的流程主要涉及以下几个步骤,这些步骤帮助管理线程的创建、执行任务、复用和销毁,从而提高程序的效率和响应速度。以下是线程池工作的大致流程:

  1. 创建线程池:通过Executors类的静态方法或直接使用ThreadPoolExecutor构造器来创建线程池。创建时可以指定线程池的核心参数,如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务队列(BlockingQueue<Runnable>)等。
  2. 提交任务:当有新任务需要执行时,通过线程池的execute(Runnable)或submit(Callable<T>)方法提交任务。execute()用于提交不需要返回值的任务,而submit()则用于提交需要返回值的任务,并且可以获取Future对象来监控任务状态和结果。
  3. 任务调度
  4. 如果当前运行的线程少于核心线程数,新提交的任务会被立即启动一个新的线程来执行。
  5. 如果当前运行的线程数等于核心线程数且任务队列未满,任务会被放入任务队列中等待执行。
  6. 如果当前运行的线程数等于核心线程数且任务队列已满,且线程数小于最大线程数,会创建新的线程来处理任务。
  7. 如果当前线程数已经达到最大线程数并且队列也满了,根据策略(如AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy等)处理超出部分的任务,默认情况下会抛出RejectedExecutionException异常。
  8. 线程复用:线程池中的线程在执行完一个任务后不会立即销毁,而是会继续从任务队列中取出下一个任务来执行,直到队列为空。如果设置了线程空闲时间,那么在空闲时间到达后,超过核心线程数的空闲线程会被终止以减少资源占用。
  9. 关闭线程池:可以通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池。shutdown()会等待所有已提交的任务完成后再关闭线程池,而shutdownNow()会尝试中断所有正在执行的任务并关闭线程池。