04_线程及线程池

一、线程

1.1创建线程的方式

在Java中,线程的创建方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口以及使用线程池(ExecutorService)。下面我将分别介绍这四种方式的优缺点。

1. 继承Thread类

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

优点

  • 简单直观:通过继承Thread类并重写run()方法,可以很容易地创建线程。
  • 代码量少:无需额外创建接口或类。

缺点

  • 单继承限制:由于Java采用单继承机制,如果一个类已经继承了其他类,则无法再继承Thread类。
  • 线程与任务耦合度高:线程和任务高度耦合,不能复用任务。

2. 实现Runnable接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程
    }
}

优点

  • 避免单继承限制:一个类可以实现多个接口,因此不会受到Java单继承的限制。
  • 线程与任务解耦:Runnable接口将线程的任务和线程本身分开,使得线程和任务可以解耦,任务可以被多个线程共享。
  • 灵活性高:设计更加灵活,符合面向对象的设计思想。

缺点

  • 需要通过Thread类启动线程:略显繁琐,但这也是设计上的一种解耦,使得任务和线程的管理更加清晰。

3. 实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result: " + Thread.currentThread().getName();
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());
        String result = future.get(); // 获取Callable执行结果
        System.out.println(result);
        executor.shutdown();
    }
}

优点

  • 支持返回值:Callable接口允许线程执行任务并返回结果,而Runnable则不支持返回值。
  • 可以抛出异常:Callable执行时可以抛出异常,便于处理任务执行中的错误。

缺点

  • 需要结合ExecutorService使用:增加了代码复杂度,但ExecutorService提供了更好的线程管理机制。
  • 阻塞问题:通过Future获取结果时,必须等待线程执行完毕,存在一定的阻塞。

4. 使用线程池(ExecutorService)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task is running: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建一个固定大小的线程池
        for (int i = 0; i < 5; i++) {
            executorService.submit(new MyTask()); // 提交任务给线程池
        }
        executorService.shutdown(); // 关闭线程池,不再接受新任务,但会继续执行已提交的任务
    }
}

优点

  • 高效管理线程:线程池能有效管理和复用线程,避免频繁创建和销毁线程的开销。
  • 处理高并发:适合高并发场景,能够高效地处理大量并发任务。
  • 配置灵活:线程池提供了丰富的配置选项,可以灵活调整线程池大小,优化资源使用。

缺点

  • 配置复杂:需要合理配置线程池的大小,不当的配置可能导致线程池资源浪费或性能瓶颈。

总结

选择哪种线程创建方式取决于具体的应用场景和需求。对于简单的任务,可以选择继承Thread类;对于需要复用任务或避免单继承限制的场景,可以选择实现Runnable接口;对于需要返回值或处理异常的任务,可以选择实现Callable接口;对于高并发或需要管理大量线程的场景,使用线程池(ExecutorService)是最佳选择。

1.2 start()和run()的区别

  • start()方法用于启动一个新线程,并异步地调用该线程的run()方法。
  • run()方法包含了线程要执行的代码,它是线程实际执行的地方。
  • 直接调用run()方法(而不是通过start()方法)会在当前线程中同步执行代码,而不是在新的线程中执行。

因此,在创建和启动线程时,应该使用start()方法,而不是直接调用run()方法。这是编写多线程Java程序时的关键区别。

ps:直接调用run()就相当于调用普通的同步方法,不涉及异步执行。

二、线程池

1. 线程池重要参数

线程池的重要参数通常指的是Java中ThreadPoolExecutor类的构造函数中所使用的参数,这些参数用于配置线程池的行为,有助于高效地管理和复用线程,减少创建和销毁线程的开销。以下是线程池的几个重要参数:

  1. corePoolSize(核心线程数)

    • 作用:线程池中保持活动的最小线程数,即使这些线程处于空闲状态,线程池也会保持它们的存在,直到任务完成或线程池关闭。
    • 用法:如果提交的任务数大于核心线程数,线程池会根据需要增加线程数,但不会超过maximumPoolSize
  2. maximumPoolSize(最大线程数)

    • 作用:线程池中允许的最大线程数。线程池的线程数不会超过这个值。
    • 用法:在队列已满的情况下,线程池会创建新的线程来处理任务,直到线程池的线程数达到这个值。
  3. keepAliveTime(空闲线程保持存活的时间)

    • 作用:超过这个时间没有任务执行的空闲线程会被销毁。
    • 用法:如果线程池的线程数超过corePoolSize,且空闲线程超过keepAliveTime,则这些空闲线程会被终止。
  4. unit(keepAliveTime的时间单位)

    • 作用:指定keepAliveTime的单位。
    • 用法:可以是秒(TimeUnit.SECONDS)、毫秒(TimeUnit.MILLISECONDS)等。
  5. workQueue(任务队列)

    • 作用:用于保存等待执行的任务。线程池通过队列来存储提交的任务,等待有空闲线程时执行。
    • 用法:如果队列已满并且线程池中线程数小于maximumPoolSize,线程池会创建新的线程来处理任务。如果线程池中的线程数已经达到最大值,则根据拒绝策略处理任务。常见的队列类型有:
      • LinkedBlockingQueue:队列大小可以无限制,任务会被加入到队列中等待。
      • ArrayBlockingQueue:队列大小有上限,任务会被限制在队列大小内,超过上限则拒绝任务。
      • SynchronousQueue:每个任务都需要有一个空闲线程来执行,队列的容量为0。每提交一个任务就必须等待一个线程来接收它。
  6. handler(拒绝策略)

    • 作用:当线程池中的线程数已经达到最大线程数,且任务队列也已满时,如何处理新提交的任务。
    • 常见策略
      • AbortPolicy:默认策略,抛出RejectedExecutionException异常。
      • CallerRunsPolicy:由调用者线程执行该任务,避免任务丢失。
      • DiscardPolicy:丢弃当前任务,不抛出异常。
      • DiscardOldestPolicy:丢弃队列中最旧的任务,并尝试提交当前任务。
  7. threadFactory(线程工厂)

    • 作用:用于创建新线程的工厂。通过线程工厂,用户可以定制线程的创建方式,如线程的名称、优先级等。
    • 默认值Executors.defaultThreadFactory(),即默认的线程工厂。
  8. allowCoreThreadTimeOut(是否允许核心线程超时)

    • 作用:是否允许核心线程在空闲时被终止。默认情况下,核心线程会一直保持活动状态,直到线程池关闭。
    • 用法:如果设置为true,即使是核心线程,也会在超出空闲时间后被终止。
ThreadPoolExecutor executor2 = new ThreadPoolExecutor(5, 
                10, 
                1000, TimeUnit.MILLISECONDS, 
                new ArrayBlockingQueue<>(10),
                new UtilityElf.DefaultThreadFactory("test", false), 
                new ThreadPoolExecutor.AbortPolicy());

这些参数共同决定了线程池的性能和行为,通过合理配置这些参数,可以优化线程池的性能,适应不同的应用场景。

2. 线程池常用任务队列详解

在Java线程池中,任务队列有多种常见类型,每种类型都有其特定的用途和代码实现方式。以下是几种主要的任务队列类型及其代码示例:

1. 无界队列(Unbounded Queue) - LinkedBlockingQueue

无界队列理论上可以容纳无限数量的任务,但实际上受限于JVM的内存。当内存耗尽时,会抛出OutOfMemoryError

原理:LinkedBlockingQueue队列容量可以指定容量也可以不指定容量。指定时为有界队列,不指定容量时其实代码中给了默认长度(Integer.MAX_VALUE=231-1)

// 无界队列构造函数
public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }
import java.util.concurrent.*;

public class UnboundedQueueExample {
    public static void main(String[] args) {
        // 创建一个无界队列
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

        // 创建一个线程池,使用无界队列
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
                queue
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}
2. 有界队列(Bounded Queue) - ArrayBlockingQueue

有界队列有一个固定的容量限制。当队列满时,尝试添加新任务的操作会被阻塞,直到队列中有空间可用。

import java.util.concurrent.*;

public class BoundedQueueExample {
    public static void main(String[] args) {
        // 创建一个有界队列,容量为5
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(5);

        // 创建一个线程池,使用有界队列
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
                queue
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}
3. 直接提交队列(SynchronousQueue)

直接提交队列不存储任务,而是将每个任务直接提交给线程处理。它相当于一个通道,将任务直接传递给线程。

import java.util.concurrent.*;

public class SynchronousQueueExample {
    public static void main(String[] args) {
        // 创建一个直接提交队列
        BlockingQueue<Runnable> queue = new SynchronousQueue<>();

        // 创建一个线程池,使用直接提交队列
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                0L, TimeUnit.MILLISECONDS, // 空闲线程保持存活的时间(这里设置为0,因为队列不存储任务)
                queue,
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(当线程池和队列都满时)
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}
4. 优先级队列(PriorityBlockingQueue)–实际执行顺序并非严格按照优先级

优先级队列支持按照任务的优先级顺序来处理任务。任务需要实现Comparable接口或提供Comparator来定义优先级。

import java.util.concurrent.*;

class PriorityTask implements Runnable, Comparable<PriorityTask> {
    private int priority;

    public PriorityTask(int priority) {
        this.priority = priority;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is executing task with priority " + priority);
    }

    @Override
    public int compareTo(PriorityTask other) {
        return Integer.compare(this.priority, other.priority);
    }
}

public class PriorityQueueExample {
    public static void main(String[] args) {
        // 创建一个优先级队列
        BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();

        // 创建一个线程池,使用优先级队列
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
                queue
        );

        // 提交不同优先级的任务给线程池
        executor.execute(new PriorityTask(1));
        executor.execute(new PriorityTask(5));
        executor.execute(new PriorityTask(3));

        // 关闭线程池
        executor.shutdown();
    }
}
  • 尽管你使用了PriorityBlockingQueue作为线程池的任务队列,但任务的执行顺序仍然可能不是严格按照优先级来的。这通常是由以下几个原因导致的:

    1. 线程并发性‌:
      • 线程池中的线程是并发执行的,这意味着多个线程可能会同时从队列中取出任务并执行。尽管PriorityBlockingQueue保证了队列中任务的优先级顺序,但多个线程同时执行时,任务的实际执行顺序可能会受到线程调度、CPU资源分配等因素的影响。
    2. 任务执行时间‌:
      • 如果不同优先级的任务执行时间差异很大,高优先级的任务可能会在低优先级的任务之后开始执行,但因为它执行得快,所以可能会在低优先级的任务之前完成。这给人一种错觉,好像任务的执行顺序不是按照优先级来的。
    3. 线程池配置‌:
      • 线程池的核心线程数和最大线程数设置也会影响任务的执行顺序。如果核心线程数很少,而任务数量很多,那么即使队列中的任务是按照优先级排序的,也可能因为线程不足而导致任务等待执行。
    4. PriorityBlockingQueue的特性‌:
      • PriorityBlockingQueue是一个无界的优先级队列,它基于堆结构实现。虽然它能够保证每次出队的都是优先级最高的任务,但在多线程环境下,多个线程可能会同时尝试出队操作,这可能导致一些竞争条件,进而影响任务的执行顺序。
    5. 任务提交顺序与优先级的关系‌:
      • 你提交的任务顺序是1, 2, 3,但如果这些任务的优先级不是按照数字大小来设置的(比如数字越大优先级越低),那么执行顺序自然就不会是1, 2, 3。确保你的PriorityTask类的compareTo方法正确实现了优先级的比较逻辑。
    6. 线程池状态管理‌:
      • 线程池在管理其内部线程和任务时,可能会因为一些内部状态管理(如线程正在执行其他任务、线程处于空闲状态但尚未被唤醒等)而导致任务执行顺序的偏差。

    为了改善这种情况,你可以尝试以下方法:

    • 确保PriorityTask类的compareTo方法正确实现了优先级的比较逻辑。
    • 调整线程池的配置,如增加核心线程数和最大线程数,以减少任务等待执行的时间。
    • 如果任务执行时间差异很大,考虑使用同步机制(使用同步工具如CountDownLatchSemaphore等)或调整任务的设计来确保高优先级的任务能够优先执行。
    • 监控线程池的性能指标,如活跃线程数、任务队列大小等,以便及时发现并解决问题。

    请注意,尽管PriorityBlockingQueue提供了优先级排序的功能,但在多线程环境下,它并不能保证任务的严格顺序执行。如果你需要严格的顺序执行,可能需要考虑使用其他同步机制或调整应用程序的设计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀才恶霸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值