Java --- 多线程

发布于:2024-12-23 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

前言:

一.线程的创建:

1.通过继承 Thread 类来创建线程:

2.通过Runnable接口创建线程:

3.通过Java8引入的lambda语法:

线程的优先级:

二.线程的生命周期:

三. 中断线程:

线程中断的关键方法:

为什么用 while 而不用 if 判断? 

四.守护线程:

五.线程的同步:

竞争条件:

1.使用 synchronized 关键字:

(1)同步实例方法:

(2)同步静态方法:

 (3)同步代码块:

2.使用 volatile 关键字:

3.线程的等待(wait)与唤醒(notify):

等待-唤醒机制基本概念:

关键方法说明

4.使用 ReentrantLock锁:

对于ReentrantLock锁的等待与唤醒:

Condition 的常用方法:

六.线程池的使用:

1.线程池的核心接口和实现:

2. 线程池的常见实现类:

 3.创建线程池:

(1)使用 Executors 工厂类创建线程池:

(2)使用 ThreadPoolExecutor 直接创建线程池:

4.线程池的常用方法:

(1)submit():

(2)shutdown() 和 shutdownNow():

(3)invokeAll() 和 invokeAny():

 七.ThreadLocal的使用:

1.什么是ThreadLocal:

2.ThreadLocal的工作原理:

 3.ThreadLocal的常见方法:

4.应用场景:

八.虚拟线程的使用:

1.什么是虚拟线程:

2.创建虚拟线程:

3.通过虚拟线程池管理虚拟线程:

4.虚拟线程的 I/O 操作:


 

Java多线程编程是并发编程的一部分,旨在通过并行执行任务提高程序的执行效率。Java提供了强大的多线程支持,包括 Thread 类和 Runnable 接口以及更高级的 Executor 服务、同步工具(如 synchronizedLock)、条件变量等。


前言:

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。


Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

因此,必须掌握Java多线程编程才能继续深入学习其他内容。

一.线程的创建:

1.通过继承 Thread 类来创建线程:

Java中可以通过继承 Thread 类来创建线程。在 Thread 类中重写 run() 方法,实现线程的执行逻辑。

// 继承 Thread 类创建线程
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的任务
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        // 启动线程
        thread1.start();
        thread2.start();
    }
}
  • Thread 类提供了一个 run() 方法,继承 Thread 类并重写 run() 方法,可以定义线程要执行的任务。
  • start() 方法会启动线程并调用 run() 方法。start() 方法会把线程放入线程调度队列,等待CPU分配时间片。

2.通过Runnable接口创建线程:

Runnable 接口是Java提供的一个函数式接口,定义了一个 run() 方法。线程可以通过实现 Runnable 接口来创建。

// 实现 Runnable 接口创建线程
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的任务
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        // 创建 Runnable 对象
        MyRunnable runnable = new MyRunnable();

        // 创建 Thread 对象,并传入 Runnable
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        // 启动线程
        thread1.start();
        thread2.start();
    }
}
  • Runnable 接口只有一个 run() 方法,因此它比继承 Thread 类更具灵活性,可以让一个任务实现多个线程。
  • Thread 的构造方法可以传入一个实现了 Runnable 接口的对象,通过 start() 启动线程。

3.通过Java8引入的lambda语法:

// 多线程
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("start new thread!");
        });
        t.start(); // 启动新线程
    }
}

线程的优先级:

我们可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。 

二.线程的生命周期:

在Java程序中,一个线程对象只能调用一次 start()方法启动新线程,并在新线程中执行 run()方法。一旦 run()方法执行完毕,线程就结束了。 

因此Java中的线程有多个状态,常见的状态有:

  1. New:线程被创建但尚未启动。
  2. Runnable:线程在就绪队列中,等待 CPU 调度。
  3. Blocked:线程正在等待获取锁,进入阻塞状态。
  4. Waiting:线程调用 wait()join() 等方法进入等待状态,直到其他线程通知。
  5. Timed Waiting:线程在指定时间内处于等待状态,超时后会被唤醒。
  6. Terminated:线程执行结束或由于异常终止。

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

三. 中断线程:

线程中断是指通知一个线程它应该停止当前的工作并尽量提前退出。线程中断并不意味着线程会立即停止执行,而是线程响应中断信号并在适当的地方进行中断处理。Java中提供了对线程中断的支持。

Java中的线程中断是通过 Thread 类的 interrupt() 方法实现的。当一个线程被中断时,线程的中断状态会被设置为 true,线程可以在自己合适的时机检查该中断标志并进行适当的处理。

线程中断的关键方法:

  1. Thread.interrupt():用于请求中断线程。如果线程正在执行阻塞操作(如 sleep()wait()join() 等),中断将抛出 InterruptedException 异常;否则只是设置线程的中断标志位为 true

  2. Thread.isInterrupted():用于检查当前线程是否被中断。

  3. Thread.interrupted():用于检查当前线程是否被中断并清除中断标志位。

class Task extends Thread {
    @Override
    public void run() {
        while (!isInterrupted()) {  // 检查线程的中断标志
            try {
                // 模拟长时间计算任务
                System.out.println("任务正在执行...");
                Thread.sleep(1000); // 可能被中断
            } catch (InterruptedException e) {
                // 如果发生中断,捕获异常并退出
                System.out.println("任务被中断,退出.");
                break;
            }
        }
    }
}

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        task.start();

        // 主线程休眠3秒后中断子线程
        Thread.sleep(3000);
        task.interrupt();  // 请求中断
    }
}

为什么用 while 而不用 if 判断? 

中断的状态并不是一次性的,它可以被多次设置。Java 中的 Thread.interrupt() 方法只是设置线程的中断标志为 true,并不会强制终止线程。线程会根据自己的逻辑判断是否响应中断。使用 while 循环可以确保即使线程在处理过程中发生了阻塞或某些逻辑判断,依然能够持续检查中断标志,及时响应外部的中断请求。

四.守护线程:

守护线程是指为其他线程提供服务的线程,在所有非守护线程结束时,守护线程也会自动终止。守护线程通常用于执行一些后台任务,如垃圾回收器或其他后台服务。

守护线程与非守护线程(用户线程)的最大区别在于,守护线程在没有任何非守护线程存活时自动退出,即使它们自己还在运行。换句话说,守护线程的生命周期是由非守护线程决定的,当所有非守护线程结束时,守护线程会被强制结束。

默认情况下,所有线程都是非守护线程(用户线程)。

我们可以通过 Thread.setDaemon(true) 来将线程设置为守护线程。

class DaemonTask extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("守护线程正在运行...");
            try {
                Thread.sleep(1000);  // 模拟执行任务
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

public class DaemonThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        DaemonTask daemonTask = new DaemonTask();
        daemonTask.setDaemon(true);  // 设置为守护线程
        daemonTask.start();

        // 主线程休眠3秒后结束
        Thread.sleep(3000);
        System.out.println("主线程结束,守护线程也会自动退出.");
    }
}

五.线程的同步:

线程同步(Thread Synchronization)是多线程编程中的一个重要概念,指的是多个线程并发访问共享资源时,为了避免出现竞争条件(Race Condition)或不一致的数据状态,必须确保线程间的执行顺序和对共享资源的访问是有序的。线程同步通常用来保证多个线程对共享数据的访问是互斥的,即在同一时刻,只有一个线程可以访问共享资源

竞争条件:

竞争条件是指多个线程并发执行时,如果没有正确的同步机制来保证线程间的顺序,可能导致不同线程对共享资源的访问互相干扰,从而造成数据不一致。

例如,下面的代码片段是一个简单的递增计数器,但由于多个线程并发操作,可能会导致计数器的值错误。

class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 非原子操作,可能导致数据不一致
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // 期望是 2000,但可能出现不一致
    }
}

在这个例子中,由于 count++ 操作并非原子操作,多个线程同时执行时,可能会发生竞争条件,导致最终的计数值不为 2000。

为了防止竞争条件的发生,我们需要对共享资源的访问进行同步。同步能够确保同一时刻只有一个线程能够访问共享资源,从而避免多个线程的操作互相干扰。

在 Java 提供了多种方式来实现线程同步。常见的同步方法有 synchronized 关键字、显式锁(如 ReentrantLock)、volatile 关键字等。

1.使用 synchronized 关键字:

synchronized 关键字是 Java 提供的最基本的同步机制,可以用来保证同一时刻只有一个线程能够访问某个代码块或方法。

synchronized 关键字有三种使用方式:

  1. 同步实例方法:将 synchronized 关键字加在实例方法上,这样每次只有一个线程能够执行该实例方法。

  2. 同步静态方法:将 synchronized 关键字加在静态方法上,这样每次只有一个线程能够执行该静态方法。

  3. 同步代码块:将 synchronized 关键字用于方法内部的代码块,只对特定代码块进行同步,减少性能开销。

(1)同步实例方法:

class Counter {
    private int count = 0;

    // 使用 synchronized 确保每次只有一个线程可以执行该方法
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // 正常输出 2000
    }
}
  • synchronized 确保了在同一时刻只有一个线程能够执行 increment() 方法。
  • 通过同步方法,count++ 操作不会被多个线程同时执行,避免了数据不一致的情况。

(2)同步静态方法:

synchronized static 确保了在同一时刻,只有一个线程能够执行 increment() 静态方法。 

class Counter {
    private static int count = 0;

    // 使用 synchronized 确保每次只有一个线程可以执行该静态方法
    public synchronized static void increment() {
        count++;
    }

    public synchronized static int getCount() {
        return count;
    }
}

public class SynchronizedStaticExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // 正常输出 2000
    }
}

 (3)同步代码块:

synchronized (this) 确保只有一个线程可以执行 increment() 方法中的关键部分。通过同步代码块,我们可以更精细地控制同步范围,减少性能开销。 

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {  // 只对特定代码块进行同步
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // 正常输出 2000
    }
}

2.使用 volatile 关键字:

volatile 关键字保证了变量的可见性,即线程修改了 volatile 变量的值,其他线程能够立即看到修改后的值。volatile 并不保证原子性,但它可以用于避免一些简单的同步问题,如标志变量的检测。

使用 volatile 保证了 flag 变量的值在不同线程间的可见性。

class SharedResource {
    private volatile boolean flag = false;

    public void setFlagTrue() {
        flag = true;  // 设置 flag 为 true
    }

    public boolean getFlag() {
        return flag;  // 读取 flag 的值
    }
}

3.线程的等待(wait)与唤醒(notify):

除了常见的线程互斥功能外,synchronized 还提供了一种 等待-唤醒机制,允许线程在特定条件下挂起,并等待其他线程的通知。

这种机制通常用于实现线程间的通信,如 生产者-消费者问题,即线程A在资源不足时进入等待状态,而线程B则在资源有了后通知线程A继续执行。

等待-唤醒机制基本概念:

  • 等待(wait():当线程执行到某一条件不满足时,使用 wait() 让该线程进入等待状态,释放持有的锁,直到其他线程通过调用 notify()notifyAll() 唤醒它。
  • 唤醒(notify()notifyAll():当某些条件改变时,其他线程调用 notify()notifyAll() 唤醒处于等待状态的线程。notify() 唤醒一个线程,notifyAll() 唤醒所有等待的线程

关键方法说明

  1. wait():使当前线程进入等待状态,释放持有的锁,直到其他线程调用 notify()notifyAll() 唤醒它。wait() 必须在同步块或同步方法中调用。使当前线程等待,并且释放持有的锁。

  2. notify():唤醒一个等待的线程。如果多个线程在等待该对象的监视器(锁),notify() 会唤醒其中一个线程(具体哪个线程无法确定)。

  3. notifyAll():唤醒所有等待的线程,所有等待该对象监视器的线程都会被唤醒。

wait()notify() 只能在同步块或同步方法中调用,因为它们依赖于对象的监视器(锁)。在同步方法内调用时,该方法的锁是当前对象;在同步代码块内,锁是传入的对象。

在调用 wait() 时,线程会释放当前的锁,允许其他线程获取锁。当该线程被唤醒后,它会重新尝试获取锁。

class BoundedBuffer {
    private int[] buffer;
    private int count = 0;  // 缓冲区中的元素个数
    private int putIndex = 0; // 下一次插入的位置
    private int takeIndex = 0; // 下一次取出的位置

    // 创建缓冲区
    public BoundedBuffer(int size) {
        buffer = new int[size];
    }

    // 生产者放入数据
    public synchronized void put(int value) throws InterruptedException {
        while (count == buffer.length) {  // 如果缓冲区满
            wait();  // 生产者等待
        }
        buffer[putIndex] = value;
        putIndex = (putIndex + 1) % buffer.length;  // 循环插入
        count++;
        notifyAll();  // 通知消费者可以取数据了
    }

    // 消费者取出数据
    public synchronized int take() throws InterruptedException {
        while (count == 0) {  // 如果缓冲区空
            wait();  // 消费者等待
        }
        int value = buffer[takeIndex];
        takeIndex = (takeIndex + 1) % buffer.length;  // 循环取出
        count--;
        notifyAll();  // 通知生产者可以放数据了
        return value;
    }
}

public class ProducerConsumer {
    public static void main(String[] args) throws InterruptedException {
        BoundedBuffer buffer = new BoundedBuffer(10);

        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    buffer.put(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    int value = buffer.take();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

4.使用 ReentrantLock锁:

在 Java 中,多线程同步的传统方法是使用 synchronized 关键字。虽然 synchronized 简单易用,但它存在一些不足之处,如无法精确控制锁的获取和释放、缺少条件等待和通知机制等。为了解决这些问题,Java 提供了显式锁机制 ReentrantLock(可重入锁)和 Condition 类,能够提供更多的灵活性和控制。

ReentrantLockjava.util.concurrent.locks 包下的一个类,它是一个可重入的互斥锁,允许在一个线程中多次获取同一个锁,并且能够提供比 synchronized 更灵活的功能。

  • ReentrantLock 提供了对 count++ 操作的显式同步,保证了并发情况下 count 的值是正确的。
  • lock.lock() 获取锁,lock.unlock() 释放锁。如果忘记调用 unlock(),可能会导致死锁问题,因此需要确保在 finally 块中释放锁。
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    // 使用 ReentrantLock 进行线程同步
    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

public class ReentrantLockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // 正常输出 2000
    }
}

对于ReentrantLock锁的等待与唤醒

Condition 是与 ReentrantLock 配合使用的一个接口,它允许线程在满足某个条件时进行等待或通知,类似于传统的 Object.wait()Object.notify() 方法。Condition 提供了更多的控制选项,可以精确控制等待和通知的条件。

Condition 的常用方法:
  1. await():使当前线程等待,直到被其他线程通知或者中断。

  2. signal():唤醒一个等待的线程,使其能够继续执行。

  3. signalAll():唤醒所有等待的线程,使它们能够继续执行。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

class BoundedBuffer {
    private final int[] buffer; // 缓冲区
    private int count = 0;
    private int putIndex = 0;
    private int takeIndex = 0;

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();  // 缓冲区不满时的条件
    private final Condition notEmpty = lock.newCondition(); // 缓冲区不空时的条件

    public BoundedBuffer(int size) {
        buffer = new int[size];
    }

    public void put(int value) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await();  // 如果缓冲区满,等待
            }
            buffer[putIndex] = value;
            if (++putIndex == buffer.length) {
                putIndex = 0;  // 环形缓冲区
            }
            count++;
            notEmpty.signal();  // 唤醒等待取数据的线程
        } finally {
            lock.unlock();
        }
    }

    public int take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();  // 如果缓冲区为空,等待
            }
            int value = buffer[takeIndex];
            if (++takeIndex == buffer.length) {
                takeIndex = 0;  // 环形缓冲区
            }
            count--;
            notFull.signal();  // 唤醒等待放数据的线程
            return value;
        } finally {
            lock.unlock();
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) throws InterruptedException {
        BoundedBuffer buffer = new BoundedBuffer(10);

        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    buffer.put(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    int value = buffer.take();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

六.线程池的使用:

在多线程编程中,创建和销毁线程是一个非常耗费资源的操作。如果频繁地创建和销毁线程,会导致性能下降,甚至可能引发系统崩溃。因此,Java 提供了 线程池 的机制来管理线程的生命周期,避免每次任务执行时都需要创建新线程。

线程池是一种常用的设计模式,可以复用已创建的线程,而不是每次都创建新线程。Java 提供了 Executor 框架来管理线程池及其任务。

1.线程池的核心接口和实现:

Executor:是线程池的核心接口,提供了执行任务的方法。

public interface Executor {
    void execute(Runnable command);
}

ExecutorService:是 Executor 的子接口,提供了更多的功能,比如管理线程池、提交任务、关闭线程池等。

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
}

ThreadPoolExecutor:是 ExecutorService 的常用实现类,提供了线程池的管理功能。它是一个可定制化的线程池实现。

ScheduledExecutorService:继承了 ExecutorService,用于执行定时任务。

2. 线程池的常见实现类:

  • FixedThreadPool:创建一个固定大小的线程池。线程池中的线程数量固定,任务会在空闲的线程上执行。适用于任务量较大、每个任务执行时间相似的场景。

  • CachedThreadPool:创建一个可缓存的线程池。如果线程池中的线程空闲超过60秒,则会被回收。适用于任务量不确定,且每个任务的执行时间较短的场景。

  • SingleThreadExecutor:创建一个只有一个线程的线程池,适用于任务顺序执行的场景。

  • ScheduledThreadPoolExecutor:用于执行定时任务,支持延时执行任务、周期性任务等

 3.创建线程池:

(1)使用 Executors 工厂类创建线程池:

Java 提供了 Executors 类来创建各种类型的线程池。以下是一些常见的线程池创建方式:

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 固定大小线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        // 可缓存线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        // 单线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        // 定时线程池
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
    }
}

(2)使用 ThreadPoolExecutor 直接创建线程池:

ThreadPoolExecutor 提供了更灵活的创建方式,可以定制线程池的各个参数。常用的构造方法如下:

ThreadPoolExecutor(
    int corePoolSize,        // 核心池大小(即使没有任务时,线程池也会保留的最小线程数)
    int maximumPoolSize,     // 最大池大小(线程池能够容纳的最大线程数)
    long keepAliveTime,      // 空闲线程存活时间(当线程池中的线程数大于 corePoolSize 时,空闲线程的最大存活时间)
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue  // 用于保存任务的阻塞队列
    // LinkedBlockingQueue:无界队列
    // ArrayBlockingQueue:有界队列
    // PriorityBlockingQueue:优先级队列
)

4.线程池的常用方法:

(1)submit():

submit() 方法可以提交一个 RunnableCallable 任务,返回一个 Future 对象,允许在任务完成后获取其结果。

ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> future = executor.submit(() -> {
    // 任务逻辑
    return 123;
});
Integer result = future.get(); // 获取任务执行结果
System.out.println(result);

(2)shutdown() 和 shutdownNow():

executor.shutdown(); // 启动关闭过程,等待所有任务完成
executor.shutdownNow(); // 尝试停止正在执行的任务

(3)invokeAll() 和 invokeAny():

  • invokeAll():一次执行多个任务,并返回所有任务的 Future 列表,等待所有任务完成。
  • invokeAny():一次执行多个任务,返回最先完成任务的结果。
List<Callable<Integer>> tasks = new ArrayList<>();
tasks.add(() -> 1);
tasks.add(() -> 2);

ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<Integer>> futures = executor.invokeAll(tasks);
for (Future<Integer> future : futures) {
    System.out.println(future.get());
}

Integer result = executor.invokeAny(tasks);  // 返回第一个完成的任务结果
System.out.println(result);

 七.ThreadLocal的使用:

ThreadLocal 是 Java 提供的一种用于解决多线程并发问题的机制。它能够为每个线程提供独立的变量副本,这样多个线程访问同一个 ThreadLocal 变量时,各自的副本不会相互影响,避免了线程安全问题。

在多线程环境下,通常多个线程会共享某些数据,但如果多个线程同时访问同一份数据,可能会引发数据竞争和线程安全问题。ThreadLocal 通过为每个线程提供独立的副本来解决这一问题。

1.什么是ThreadLocal:

ThreadLocal 提供了一个每个线程私有的变量副本。每个线程在首次访问 ThreadLocal 时,会为该线程创建一个 ThreadLocal 变量的副本。之后同一线程访问该变量时,将直接操作该线程的副本,而不会与其他线程的副本发生冲突。

每个线程可以通过 ThreadLocal 变量独立地获取、设置自己的值。

2.ThreadLocal的工作原理:

ThreadLocal 工作的核心是 线程隔离。每个线程都有自己独立的 ThreadLocal 变量副本,访问这个副本的操作不会影响其他线程的副本。每个线程都有一个与 ThreadLocal 对象关联的 线程本地存储

具体而言:

  • 当线程第一次访问 ThreadLocal 变量时,系统会创建该线程本地的副本并初始化它。
  • 每个线程可以通过 ThreadLocal 类提供的 get()set() 方法来获取和设置它的本地副本。
  • 不同线程间互不干扰,ThreadLocal 实现了线程数据隔离。

 3.ThreadLocal的常见方法:

(1)get():获取当前线程对 ThreadLocal 变量的副本。

public T get();

(2)set(T value):设置当前线程对 ThreadLocal 变量的副本。

public void set(T value);

(3)initialValue():返回当前线程对 ThreadLocal 变量的初始值。如果没有设置初始值,会调用此方法生成。

protected T initialValue();

4.应用场景:

在下面的例子中:

  • threadLocal 是一个 ThreadLocal<Integer> 变量,它为每个线程维护了一个独立的副本。
  • 通过 threadLocal.get()threadLocal.set(),每个线程操作的是自己私有的副本,互不干扰。
  • 线程1和线程2的初始值都是 1,但是它们在各自的上下文中可以独立地修改这个值。
public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) {
        // 启动多个线程来演示 ThreadLocal 的使用
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 initial value: " + threadLocal.get());  // 1
            threadLocal.set(2);
            System.out.println("Thread 1 new value: " + threadLocal.get());     // 2
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 initial value: " + threadLocal.get());  // 1
            threadLocal.set(3);
            System.out.println("Thread 2 new value: " + threadLocal.get());     // 3
        });

        thread1.start();
        thread2.start();
    }
}

ThreadLocalwithInitial 方法允许你指定一个初始值。如果线程没有显式设置值,ThreadLocal 会使用该初始值。

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Default Value");

public class ThreadLocalExample {
    public static void main(String[] args) {
        System.out.println(threadLocal.get());  // 输出: Default Value
    }
}

八.虚拟线程的使用:

1.什么是虚拟线程:

虚拟线程是 JEP 436 中提出的一项新特性,旨在简化 Java 并发编程并提高应用程序的可扩展性。虚拟线程是基于 轻量级线程(Lightweight Threads)设计的,它们由 Java 虚拟机(JVM)进行调度,而不是由操作系统进行调度。虚拟线程的引入将有助于大规模并发编程,特别是在需要大量并发线程的应用场景中(如 Web 服务器、异步 I/O 等)。

虚拟线程的目标是提供与传统的操作系统线程(称为“平台线程”)相同的编程模型,但它们在系统资源的使用上更加高效,从而显著提高程序的并发性能。

2.创建虚拟线程:

虚拟线程可以通过 Thread.ofVirtual() 方法创建,或者通过 ExecutorService 来管理虚拟线程池。

public class VirtualThreadExample {
    public static void main(String[] args) {
        Thread virtualThread = Thread.ofVirtual().start(() -> {
            System.out.println("Virtual thread is running!");
        });

        // 等待线程执行完毕
        try {
            virtualThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.通过虚拟线程池管理虚拟线程:

我们可以使用 ExecutorService 来管理虚拟线程池,这样可以简化多线程管理。

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

public class VirtualThreadExecutorExample {
    public static void main(String[] args) {
        // 创建一个虚拟线程池
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        // 提交任务到虚拟线程池
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running on virtual thread.");
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

4.虚拟线程的 I/O 操作:

虚拟线程特别适合 I/O 密集型操作。使用虚拟线程时,JVM 会自动挂起不活跃的线程并将 CPU 资源分配给其他线程,从而避免了传统线程模型中因为 I/O 操作导致线程空闲的资源浪费。

import java.util.concurrent.*;

public class IOBlockingExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    System.out.println("Task " + taskId + " starts I/O operation.");
                    // 模拟 I/O 操作
                    Thread.sleep(1000);
                    System.out.println("Task " + taskId + " finishes I/O operation.");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
    }
}