Java技术学习笔记-----线程篇(一)

59 阅读6分钟

什么是线程

线程(英语:thread)是进程的基本组成单位,一个进程可以包含多个线程,每个线程可以并行执行不同的任务。


线程与进程的区别

  1. 根本区别:进程是操作系统(OS)分配资源的基本单位,而线程是处理器(CPU)任务调度和执行的最基本单位。也就是说,进程是程序的一次执行过程,它占有独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。而线程是进程中的一个执行流程,不同的线程共享同一份进程的地址空间。
  2. 资源管理:同一进程内的不同线程共享该进程的资源,例如内存、I/O、CPU等。因此线程之间的通信更容易,编程也更简单。然而,因为多个线程共享同一进程的资源,所以不利于资源的管理和保护。相反,进程之间的资源是独立的,能很好地进行资源管理和保护。
  3. 开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器。
  4. 健壮性:多进程因为资源完全独立,一个进程的崩溃不会影响到其他进程。而在多线程环境中,一个线程的崩溃可能会影响到整个进程中的其他线程。

线程创建常用的四种方式

1)继承 Thread类 创建线程

步骤

  1. 创建一个类,继承自Thread类。
  2. 重写run()方法,将需要执行的任务放在run()方法中。
  3. 创建该类的实例对象。
  4. 调用start()方法启动线程
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2)实现 Runnable 接口创建线程

步骤

  1. 创建一个类,实现Runnable接口。
  2. 重写run()方法,将需要执行的任务放在run()方法中。
  3. 创建该类的实例对象。
  4. 使用Thread类的构造方法创建线程对象,并将Runnable实例作为参数传入。
  5. 调用start()方法启动线程。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

3)使用 Callable 和 Future 创建线程

步骤

  1. 创建一个实现Callable接口的类,重写call()方法,将需要执行的任务放在call()方法中。
  2. 创建该类的实例对象。
  3. 使用ExecutorServicesubmit()方法提交任务,返回一个Future对象。
  4. 调用Future对象的get()方法获取任务执行结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "MyCallable is running";
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyCallable myCallable = new MyCallable();
        Future<String> future = executorService.submit(myCallable);
        try {
            System.out.println(future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

4)使用线程池,例如用 Executor 框架

步骤

  1. 创建一个实现Runnable接口的任务类。
  2. 使用Executors工厂方法创建一个固定大小的线程池。例如,创建一个包含5个线程的线程池:。
  3. 通过executor.execute(new MyTask())将任务提交给线程池执行。
  4. 当所有任务都提交后,调用shutdown()方法来关闭线程池。
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务正在执行:" + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Executor executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(new MyTask());
        }
        executor.shutdown();
    }
}

什么是 Executor框架

Executor框架在Java 5之后引入,最大优点是将任务的提交和执行解耦

Executor框架主要由三部分组成:任务、任务的执行和异步计算的结果。

  1. 任务:这是工作单元,包括Runnable接口和Callable接口,它们是被执行任务需要实现的接口。分别代表了"可以运行的任务"和"可以返回结果的任务"。
  2. 任务的执行:这是把任务分派给多个线程的执行机制,核心接口为Executor,以及ExecutorService接口。
  3. 异步计算的结果:包括Future接口和实现了Future接口的FutureTask类。通过Future对象,我们能获取到异步任务执行的结果。

ExecutorService接口有两个关键实现类:ThreadPoolExecutorScheduledThreadPoolExecutor

  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务
  • ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。

什么是线程池? 使用它有哪些好处?

线程池是一种用于管理和复用线程的机制,它允许在应用程序中创建一组可用线程,并在需要执行任务时将任务分配给这些线程。

线程池的主要目的是优化线程的创建、销毁和管理,以提高多线程应用程序的性能和效率。

可以把线程池理解为公司(或者管理机制),线程理解为员工,任务理解为公司的工作

具体的好处如下

  1. 降低线程创建和销毁的开销:线程的创建和销毁是开销较大的操作,线程池可以重复利用线程,减少这些开销。 (理解为公司不会每个任务都去招新人、用完再开除)
  2. 控制并发度:通过配置线程池的大小,可以限制并发执行的任务数量,防止系统过载。
  3. 统一管理和监控:线程池提供了统一的管理和监控接口,方便对线程的状态和执行情况进行监控和调整。
  4. 避免资源竞争:在多线程环境中执行任务,可能会导致资源竞争和线程冲突,线程池可以帮助避免这些问题。

如何自定义线程池

可以用 ThreadPoolExecutor类 自定义线程池,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池。

其中,自定义线程池的核心参数如下:

  1. 核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用corePoolSize方法来设置。一般情况下,可以根据系统的资源情况和任务的特性来设置合适的值。
  2. 最大线程数 (maximumPoolSize): 线程池中允许存在的最大线程数。可以使用maximumPoolSize方法设置。如果所有线程都处于活动状态,而此时又有新的任务提交,线程池会创建新的线程,直到达到最大线数。
  3. 空闲线程存活时间 (keepAliveTime) : 当线程池中的线程数量超过核心线程数时,如果这些线程在一定时内没有执行任务,则这些线程会被销毁。可以使用keepAliveTimeTimeUnit方法来设置。
  4. 阻塞队列 (workQueue): 用于存放等待执行的任务的阻塞队列。可以根据任务的特性选择不同类型的队列,如LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界阻塞队列,即LinkedBlockingQueue,但也可以根据需要设置有界队列。
  5. 线程工厂 (threadFactory): 用于创建线程的工厂。可以通过实现ThreadFactory接口自定义线程的创建逻辑。
  6. 拒绝策略 (rejectedExecutionHandler) : 当线程池无法接受新的任务时,会根据设置的拒绝策略进行处理。常见的拒绝策略有 AbortPolicy、DiscardPolicy、DiscardOldestPolicy 和 CallerRunsPolicy