多线程

53 阅读28分钟

什么是多线程

多线程是一种在计算机编程中常用的并发编程技术,它允许一个程序同时执行多个独立的线程(线程是一个轻量级的执行单元),从而实现更高效的任务处理和资源利用。简单说,多线程允许一个程序在同一时间内执行多个任务,这些任务可以并行运行,从而提高了程序的响应速度和性能。


多线程中的知识点

线程和进程的区别

线程(Thread)和进程(Process)是操作系统中用于管理和执行程序的两个基本概念,它们之间有一些重要的区别:

  1. 定义:

    • 进程:进程是一个独立的执行环境,包含了程序代码、数据和系统资源的拷贝,它是操作系统分配资源和执行任务的基本单位。
    • 线程:线程是进程内的一个执行单元,它共享相同的内存空间和资源,包括代码段、数据段和打开的文件等,但拥有独立的执行路径。
  2. 资源占用:

    • 进程:每个进程都有自己独立的内存空间,因此进程之间的资源相互隔离,但创建和销毁进程通常需要较多的系统资源。
    • 线程:线程共享所属进程的内存空间,因此它们之间可以更方便地共享数据,但也容易导致线程间的资源冲突。
  3. 创建和销毁:

    • 进程:创建和销毁进程通常较为耗费资源和时间,因为需要为每个进程分配独立的内存空间。
    • 线程:创建和销毁线程通常更加轻量,因为它们共享同一进程的资源。
  4. 切换开销:

    • 进程:进程之间的切换开销较大,因为需要保存和恢复整个进程的状态。
    • 线程:线程之间的切换开销较小,因为它们共享相同的地址空间,只需切换部分状态即可。
  5. 通信和同步:

    • 进程:进程间通信(IPC)较为复杂,需要使用特殊的机制,如管道、消息队列、共享内存等。
    • 线程:线程之间通信较为简单,可以直接访问共享内存,但需要小心处理同步问题,如使用互斥锁和信号量。
  6. 安全性:

    • 进程:进程之间相对较为安全,因为它们拥有独立的内存空间,不容易相互影响。
    • 线程:线程之间需要更小心地处理共享资源,容易出现数据竞争和同步问题,因此需要更精细的同步机制。

总结来说,线程是进程内的执行单元,它们共享进程的资源,因此更轻量,但需要更仔细地处理同步和共享资源问题。进程是操作系统分配资源的基本单位,拥有独立的内存空间,因此更安全,但创建和销毁进程开销较大。选择使用线程还是进程取决于具体的应用场景和需求。


在JAVA语言中,多线程的创建方式

多线程可以使用不同的方式来创建和管理,以下是四种常见的多线程创建方式:

  1. 继承Thread类

    • 创建一个继承自java.lang.Thread类的子类,并重写run()方法来定义线程执行的任务。然后,创建子类的实例并调用start()方法启动线程。
    java
    复制代码
    class MyThread extends Thread {
        public void run() {
            // 线程执行的任务
        }
    }
    
    MyThread thread = new MyThread();
    thread.start();
    
  2. 实现Runnable接口

    • 创建一个实现java.lang.Runnable接口的类,实现其run()方法来定义线程执行的任务。然后,创建一个Thread对象,并将Runnable对象传递给它,最后调用start()方法启动线程。
    java
    复制代码
    class MyRunnable implements Runnable {
        public void run() {
            // 线程执行的任务
        }
    }
    
    Runnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    
  3. 实现Callable接口(带返回值的线程):

    • 创建一个实现java.util.concurrent.Callable接口的类,实现其call()方法来定义线程执行的任务,并可以返回一个结果。使用ExecutorService来提交Callable任务并获取执行结果。
    java
    复制代码
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    class MyCallable implements Callable<String> {
        public String call() {
            // 线程执行的任务,并返回结果
            return "Task completed";
        }
    }
    
    ExecutorService executor = Executors.newFixedThreadPool(2);
    Callable<String> callable = new MyCallable();
    Future<String> future = executor.submit(callable);
    
  4. 使用线程池

    • 线程池是一种更高级的多线程管理方式,它可以重复使用线程来执行多个任务。使用ExecutorService接口来创建和管理线程池,然后通过submit()方法提交任务。
    java
    复制代码
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    ExecutorService executor = Executors.newFixedThreadPool(2); // 创建一个包含两个线程的线程池
    Runnable runnable = () -> {
        // 线程执行的任务
    };
    executor.submit(runnable);
    

这些方式都可用于创建多线程,具体的选择取决于你的需求和设计。线程池是一种高效的方式,可以减少线程创建和销毁的开销,并更好地管理线程的生命周期。同时,使用Callable接口可以获得任务的执行结果,而Runnable则用于执行无需返回结果的任务。


线程池的工作流程

线程池是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源管理效率。以下是典型的线程池的工作流程:

  1. 初始化线程池

    • 创建一个线程池并初始化其参数,包括最小线程数、最大线程数、任务队列大小、线程空闲时间等。线程池的大小通常根据应用需求和系统资源来确定。
  2. 提交任务

    • 当需要执行任务时,将任务提交给线程池。任务可以是一个RunnableCallable对象,表示需要执行的工作单元。
  3. 任务队列

    • 线程池维护一个任务队列,所有提交的任务都会排队在这个队列中等待执行。如果线程池中有可用的线程,它们会从队列中取出任务并执行。如果没有可用线程,任务会等待,直到有线程可用。
  4. 线程执行任务

    • 线程池中的线程会循环地从任务队列中取出任务并执行它们。一旦任务完成,线程将返回线程池中,准备执行下一个任务。
  5. 线程复用

    • 线程池会复用线程,而不是在每个任务之后销毁线程。这减少了线程创建和销毁的开销,提高了执行效率。
  6. 线程池管理

    • 线程池负责管理线程的数量和状态。它可以根据需要动态调整线程数量,以适应不同的工作负载。例如,可以根据队列中的任务数量来增加或减少线程的数量。
  7. 任务完成

    • 当任务执行完成后,可以获取任务的执行结果(如果任务是Callable类型的)。然后可以对结果进行处理或返回给调用者。
  8. 关闭线程池

    • 当不再需要线程池时,应该显式地关闭它。关闭线程池会停止接受新任务,并等待已提交的任务执行完成。然后线程池中的线程会被终止。关闭线程池是为了释放资源并避免内存泄漏。

线程池的主要优点在于可以有效地管理和复用线程,降低了线程创建和销毁的开销,提高了应用程序的性能和响应速度。它还可以控制并发线程的数量,避免资源耗尽问题。因此,在多线程应用程序中,使用线程池通常是一种良好的实践。

以下是一个使用Java演示线程池的简单示例。在这个示例中,我们将创建一个固定大小的线程池,然后提交一些任务给线程池执行。

java
复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池,包含两个线程
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任务给线程池执行
        for (int i = 1; i <= 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running on thread " + Thread.currentThread().getName());
                // 模拟任务执行时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task " + taskNumber + " is completed.");
            });
        }

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

在这个示例中,我们使用Executors.newFixedThreadPool(2)创建了一个包含两个线程的线程池。然后,我们提交了5个任务给线程池执行。每个任务都会打印出当前线程的名称,并模拟执行任务的时间。最后,我们调用executor.shutdown()来关闭线程池。

运行此示例,你将看到线程池中的两个线程交替执行这五个任务,线程复用的特点使得线程创建和销毁的开销较小,提高了执行效率。

image.png

image.png


多线程中对的几种状态

在Java中,多线程可以具有以下几种不同的状态:

  1. New(新建)状态

    • 当线程对象被创建但尚未调用其start()方法时,线程处于新建状态。
  2. Runnable(可运行/就绪)状态

    • 当线程对象被创建,并且start()方法被调用后,线程进入可运行状态。在可运行状态下,线程可以被调度执行,但并不一定立即开始执行,它可能会在等待CPU资源。
  3. 运行状态(Running)

    • 当线程获得CPU时间并正在执行任务时,线程处于运行状态。只有处于运行状态的线程才能真正执行任务。
  4. Blocked(阻塞)状态

    • 线程在阻塞状态下会等待某些条件的满足,例如等待锁或等待输入/输出完成。当条件满足时,线程将从阻塞状态转换为可运行状态。
  5. Terminated(终止/死亡)状态

    • 线程进入终止状态,表示线程已经执行完毕或者因异常而提前终止。一旦线程终止,它不能再次回到可运行状态。

线程的状态转换是由操作系统和Java虚拟机自动管理的,开发者通常只需要关注线程的创建和任务执行。理解线程状态有助于调试和分析多线程程序中的问题,特别是在处理并发问题时。

image.png

多线程安全问题

共享数据不安全

加锁

Synchronized锁 与 Lock锁

Synchronized 与 Lock 的区别?

SynchronizedLock(java.util.concurrent.locks.Lock 接口)都是用于多线程编程中实现同步的机制,但它们有一些重要的区别:

  1. 使用方式

    • Synchronized 是 Java 语言的关键字,可以直接应用于方法或代码块中,而不需要显式地创建锁对象。例如,你可以使用synchronized关键字来实现方法级别的同步或同步代码块。
    java
    复制代码
    public synchronized void synchronizedMethod() {
        // 同步方法
    }
    
    • Lock 是一个接口,需要使用其实现类(如ReentrantLock)来创建锁对象,然后使用锁对象来进行同步操作。使用Lock提供了更多的灵活性和控制,例如可以实现可重入锁、定时锁等。
    java
    复制代码
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // 同步代码块
    } finally {
        lock.unlock();
    }
    
  2. 异常处理

    • 当使用 Synchronized 同步块时,如果在同步块中抛出异常,Java 会自动释放锁,确保锁不会一直被持有。这是因为 Synchronized 块在进入和退出时会隐式地释放锁。
    • 在使用 Lock 同步时,必须手动在 finally 块中调用 unlock() 方法来释放锁。这要求程序员更加谨慎地处理异常情况。
  3. 可中断性

    • Lock 提供了可中断性,即在等待锁的过程中可以响应中断。你可以在等待锁时,通过lockInterruptibly()方法来允许线程在等待过程中响应中断,而不会一直阻塞。
    java
    复制代码
    Lock lock = new ReentrantLock();
    try {
        lock.lockInterruptibly(); // 可中断地等待锁
        // 同步代码块
    } catch (InterruptedException e) {
        // 处理中断
    } finally {
        lock.unlock();
    }
    
    • Synchronized 不能被中断,如果线程在等待锁时被中断,它会一直等待获取锁,不会响应中断。
  4. 性能

    • 在某些情况下,Lock 可以提供更好的性能,特别是在高并发情况下。由于 Synchronized 会引入更多的竞争和上下文切换,因此在高度竞争的情况下,使用 Lock 可能更有效。

总的来说,Synchronized 更简单易用,适用于大多数情况下的同步需求,而 Lock 提供了更多的控制和功能,适用于一些特殊的同步场景,但也需要更谨慎的使用和管理。选择哪种同步机制取决于具体的需求和性能要求。

image.png

Synchronized锁

Synchronized(也称为关键字 synchronized)是Java中用于实现线程同步的关键字。它可以用来创建临界区,确保只有一个线程可以同时访问临界区内的代码块。以下是关于 Synchronized 锁的简要讲解:

  1. 作用范围

    • Synchronized 可以应用于方法级别或代码块级别。
    • 方法级别的 Synchronized 锁定整个方法,使得只有一个线程可以执行整个方法。
    • 代码块级别的 Synchronized 锁定指定的代码块,使得只有一个线程可以同时执行该代码块。
  2. 实例级别和类级别

    • 在实例方法上使用 Synchronized 锁时,锁定的是实例对象,只有一个线程可以访问同一个实例对象的同步方法。
    • 在静态方法上使用 Synchronized 锁时,锁定的是类的 Class 对象,只有一个线程可以访问同一个类的同步静态方法。
  3. 重入性

    • Synchronized 具有重入性,也就是说,同一个线程可以多次获得同一个锁。这意味着如果一个线程已经获得了某个对象的锁,那么它可以继续获得该对象的锁,而不会被阻塞。
  4. 排他性

    • Synchronized 锁是排他锁,即一次只允许一个线程获得锁。如果多个线程尝试获得同一个锁,其中一个线程会获得锁,而其他线程会被阻塞,直到锁被释放。
  5. 释放锁

    • 当线程退出 Synchronized 方法或代码块时,它会释放锁,允许其他线程获得锁并继续执行。
  6. 解决竞态条件

    • Synchronized 锁是一种用于解决竞态条件(Race Condition)的机制。通过在关键代码块上添加 Synchronized,可以确保多个线程不会同时进入临界区,从而避免数据不一致性和并发问题。

示例代码:

java
复制代码
public class SynchronizedExample {
    private int count = 0;

    // 同步实例方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void performSomeTask() {
        synchronized (this) {
            // 这个代码块是同步的
            count++;
        }
    }
}

Synchronized 是一种简单而有效的同步机制,但需要注意,它可能会引入一些性能开销,因为每次只有一个线程可以执行同步块内的代码。在某些高并发场景下,可能需要考虑使用更高级的同步机制,如 java.util.concurrent 包中的锁。

Lock锁

Lock 是 Java 中用于实现线程同步的更灵活和强大的机制。与 Synchronized 关键字相比,Lock 提供了更多的功能和控制,允许程序员显式地获取和释放锁。以下是关于 Lock 锁的简要讲解:

  1. 接口定义

    • Lock 是一个接口,它定义了锁的基本操作,主要有 lock()unlock()tryLock() 等方法。
  2. 锁的实现

    • Java 提供了多种锁的实现,最常用的是 ReentrantLock,它是可重入锁,允许同一个线程多次获得同一个锁。
  3. 显式获取和释放锁

    • Synchronized 不同,使用 Lock 需要显式地调用 lock() 方法来获取锁,以及在合适的地方调用 unlock() 方法来释放锁。这为程序员提供了更多的控制权。
  4. 重入性

    • ReentrantLock 具有重入性,允许同一个线程多次获得同一个锁。这意味着线程在持有锁的情况下可以重复进入同一个锁保护的代码块。
  5. 条件变量

    • ReentrantLock 提供了条件变量(Condition)的支持,可以使用 newCondition() 方法创建条件对象,进一步控制线程的等待和通知。
  6. 可中断性

    • Lock 支持可中断性,线程可以在等待锁的过程中响应中断。例如,可以使用 lockInterruptibly() 方法来允许线程在等待锁的过程中被中断。
  7. 非阻塞尝试

    • Lock 提供了 tryLock() 方法,允许线程尝试获取锁而不会被阻塞。如果锁已被其他线程持有,tryLock() 会返回 false
  8. 性能和灵活性

    • Lock 锁提供了更好的性能和灵活性,尤其在高并发环境中。它允许程序员更细粒度地控制同步,可以选择不同的公平性策略、超时策略等。

示例代码:

java
复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

Lock 提供了更多的控制和功能,适用于一些需要更灵活同步策略的场景。但也需要注意,使用 Lock 需要更谨慎,确保在正确的地方获取和释放锁,以避免死锁和其他问题。

锁的分类

1、悲观锁与乐观锁
复制代码
悲观锁
Synchronized
Lock
数据库的行锁、表锁
  1. Synchronized

    • Synchronized 是 Java 中的关键字,用于实现悲观锁。它用于在多线程环境下保护共享资源,确保只有一个线程可以同时访问临界区内的代码。通过在方法或代码块上添加 synchronized 关键字,可以实现线程同步。
  2. Lock

    • Lock 是 Java 中的接口,用于实现更灵活的线程同步。Lock 的实现类,如 ReentrantLock,提供了更多的控制和功能,如可中断性、定时锁、公平锁等。程序员需要显式地获取和释放锁,提供了更多的灵活性。
  3. 数据库的行锁

    • 数据库的行锁是一种数据库锁定机制,用于保护数据库表中的行级数据。当一个事务获取了某行的行锁时,其他事务必须等待该行的行锁释放才能访问或修改该行的数据。行锁用于处理并发访问数据库中相同行的情况,以确保数据一致性。
  4. 数据库的表锁

    • 数据库的表锁是一种数据库锁定机制,用于锁定整个数据库表。当一个事务获取了某表的表锁时,其他事务无法访问该表中的任何数据。表锁通常用于处理大规模数据操作,例如表重建或备份,以确保数据的完整性。
  • SynchronizedLock 代表在编程中实现线程同步的机制,用于保护共享资源免受并发访问的影响。
  • 数据库的行锁和表锁代表在数据库管理系统中实现数据访问控制的机制,用于处理多个事务同时访问数据库中的数据。行锁用于保护单个行的数据,而表锁用于保护整个表的数据。

markdown
复制代码
    乐观锁
    cas
    比较和交换
    数据库使用version字段
    

CAS(Compare and Swap)

CAS 是一种乐观锁的实现方式,它适用于多线程环境下,确保对共享数据的操作是原子的。CAS 操作包括以下两个步骤:

  1. 比较(Compare) :首先,CAS 会比较共享变量的当前值与预期值是否相等。如果相等,说明共享变量的值没有被其他线程修改,进入下一步。如果不相等,CAS 操作失败。
  2. 交换(Swap) :如果比较步骤成功,CAS 会尝试将共享变量的值更新为新的值。这个更新操作是原子的,即在执行交换的过程中不会被其他线程中断。如果更新成功,CAS 操作完成;如果失败,CAS 操作重新执行。

CAS 操作通常用于实现线程安全的数据结构和算法,例如 Java 中的 java.util.concurrent.atomic 包中的原子类,如 AtomicIntegerAtomicLong 等。

数据库中使用版本字段

在数据库中,使用版本字段(Versioning)代表一种乐观锁的实现方式。每个数据库记录(行)通常包含一个版本字段,这个字段存储了记录的版本号或时间戳。

  • 当一个事务读取数据时,它会记录下读取时的版本号或时间戳。
  • 当同一个事务尝试更新数据时,它会比较要更新的数据的当前版本号与读取时记录的版本号是否相同。
  • 如果版本号相同,表示数据在读取后没有被其他事务修改,事务可以执行更新。
  • 如果版本号不同,表示数据在读取后已经被其他事务修改,事务更新操作失败,需要进行冲突处理。

使用版本字段可以实现乐观锁,它假设并发冲突的概率较低,因此不会立即锁定数据,而是在提交更新时才检查冲突。这种方式适用于读操作频繁、写操作较少的场景,可以减少锁的争用,提高并发性能。数据库中的版本字段通常是用于记录数据的版本信息,帮助数据库管理系统检测并发冲突。


死锁

死锁产生的原因: 1、系统资源不足;

2、进程运行推进的次序不合适;

3、资源分配不当。

4、如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

死锁产生的原因及四个必要条件

1、互斥条件:一个资源一次只能被一个进程访问。

2、请求与保持: 一个进程因请求资源而阻塞时,对已获得的资源保持不放。

3、不可剥夺:进程已获得的资源,在未使用完之前,不得强行剥夺。

4、循环等待:若干进程之间形成一种头尾相接的循环等待资源关系

线程通信

线程通信和进程间通信是多线程和多进程编程中的关键概念,它们描述了不同线程或进程之间如何协作和交换信息的方式。以下是对同步通信、异步通信和进程间通信的简要解释:

同步通信

wait、notify、notifyAll、jion方法、通过 volatile 关键字、

同步通信是指线程或进程之间通过一种协作方式,以确保某个操作在另一个操作完成之后才能继续进行。这种通信方式用于实现线程或进程之间的协同工作,以确保数据的一致性和正确性。

  • 示例:在多线程编程中,可以使用锁、条件变量或信号量等同步机制来实现同步通信。例如,一个线程等待另一个线程完成某个任务后才能继续执行。

异步通信

消息中间件

异步通信是指线程或进程之间的操作是独立的,不需要等待其他操作的完成。在异步通信中,操作会立即返回,而不会阻塞当前线程或进程。异步通信通常用于提高系统的响应性和并发性。

  • 示例:在多线程编程中,可以使用回调函数、事件驱动机制或消息队列等方式来实现异步通信。例如,一个线程可以启动一个异步任务并立即返回,当任务完成后,可以通过回调函数或事件通知来处理结果。

进程间通信

http、feign、socket、mq

进程间通信是指不同的进程之间进行数据交换和协作的过程。在操作系统中,进程是独立的执行单元,它们有自己的内存空间。进程间通信允许不同进程之间共享数据和通信,以完成各种任务。

  • 示例:进程间通信可以通过多种方式实现,包括管道、套接字、消息队列、共享内存、信号量、RPC(远程过程调用)等。例如,两个不同的进程可以通过套接字建立网络连接来进行通信,或者通过共享内存来共享数据。

总结:

  • 同步通信用于确保操作的顺序和一致性,通常需要等待其他操作完成。
  • 异步通信用于提高响应性和并发性,允许操作在后台执行,不会阻塞当前线程或进程。
  • 进程间通信允许不同进程之间进行数据交换和协作,通常使用操作系统提供的通信机制来实现。

6、多线程并发问题

一、线程安全的特性

原子性:一个线程操作是不能被其他线程打断

有序性:线程在执行程序是有序的

可见性:一个线程修改数据后,对其他线程是可见的

volatile

保证可见性,可以在一个线程修改了共享数据时对其他线程可见,其原理就是修改了变量副本值后及时同步到主内存,其他线程从主内存中获取

  1. 保证有序性,会禁止指令重排

指令重排:Java代码翻译成class文件后,最终在JVM执行时,是把class翻译一个个的指令进行执行的。而CPU为了程序的执行性能,会对指令进行重新排序

也就是说万一翻译后的指令是123,那么重排后的指令可能就是213。在多线程情况下,就会出现变量的可见性问题

  1. 是基于 内存屏障 来保证
  • 有序性:内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
  • 内存屏障提供了避免重排序的功能
  • 可见性:内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
  • 内存屏障会把线程把工作内存中改变后的数据直接刷回主内存。其他线程就可以从主内存中获取最新数据

单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一种全局访问该实例的方式。单例模式通常用于需要在整个应用程序中共享一个唯一对象的情况,以避免多次创建相同对象,节省系统资源并确保数据一致性。

单例模式的核心思想是:

  1. 一个应用中,该实例有且只有一个
  2. 私有化构造函数:确保外部不能通过构造函数创建多个对象。
  3. 提供一个全局访问点:通过静态方法或静态属性,提供一个获取单例对象的途径。

以下是一个简单的单例模式示例(饿汉式):

java
复制代码
public class Singleton {
    // 私有静态成员变量,用于保存唯一的实例
    private static Singleton instance = new Singleton();

    // 私有构造函数,防止外部创建新的实例
    private Singleton() {
        // 初始化操作
    }

    // 提供全局访问点,返回单例实例
    public static Singleton getInstance() {
        return instance;
    }
}

在上面的示例中,Singleton 类的构造函数是私有的,外部无法直接创建新的实例。而通过静态方法 getInstance(),可以获取唯一的 Singleton 实例。

单例模式有多种实现方式,包括饿汉式(在类加载时就创建实例)、懒汉式(在需要时才创建实例)、双重检查锁式等。选择哪种方式取决于具体的需求和性能考虑。

单例模式的优点包括:

  • 节省资源:因为只有一个实例,不会多次创建相同对象,节省内存和其他资源。
  • 数据共享:可以在整个应用程序中共享单例对象,确保数据一致性。
  • 全局访问:通过全局访问点,简化了对象的管理和调用。

然而,单例模式也有一些潜在的缺点,如可能引入全局状态、不易扩展等,因此在使用时需要谨慎考虑。

单例模式的双重校验锁(Double-Checked Locking)是一种用于延迟初始化的单例模式实现方式,它在多线程环境下保证只创建一个实例,并且延迟到实际需要时才进行初始化。这种方式在性能和线程安全之间取得了一定的平衡。

下面是使用双重校验锁实现的单例模式的示例(基于懒汉式):

java
复制代码
public class Singleton {
    // 私有静态成员变量,用于保存唯一的实例
    private volatile static Singleton instance;

    // 私有构造函数,防止外部创建新的实例
    private Singleton() {
        // 初始化操作
    }

    // 提供全局访问点,返回单例实例
    public static Singleton getInstance() {
        if (instance == null) {
            // 第一次检查,如果实例为空,才进入同步块
            synchronized (Singleton.class) {
                // 第二次检查,确保在获取锁后实例仍然为空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里的关键点包括:

  1. 私有静态成员变量 instance,使用 volatile 关键字修饰,确保多线程环境下对 instance 的可见性。
  2. getInstance 方法中,首先进行第一次检查,如果 instance 不为空,就直接返回已创建的实例,避免不必要的同步开销。
  3. 如果 instance 为空,进入同步块,然后再次检查 instance 是否为空,这是为了防止多个线程同时通过第一次检查,导致多次创建实例。

这种双重校验锁的方式在多线程环境下能够保证线程安全,同时也避免了不必要的同步开销,因为只有在第一次创建实例时才需要同步。这使得在绝大多数情况下,多个线程可以高效地访问单例对象。

需要注意的是,双重校验锁的实现需要确保 JDK 版本在1.5及以上,因为在1.4及以下的版本中,volatile 关键字的语义不够强大,可能会导致双重校验锁失效。因此,确保使用双重校验锁的线程安全性需要考虑到 JVM 版本的兼容性。


Spring集成线程池

@Async:标记在方法上, 表示当前方法是异步执行的

@EnableAsync:开启多线程异步

Spring框架提供了对线程池的支持,你可以轻松地在Spring应用中集成和配置线程池。线程池的集成通常涉及以下几个关键步骤:

  1. 添加Spring依赖: 首先,确保你的项目中已经引入了Spring框架的依赖。你可以使用Maven、Gradle等构建工具来管理依赖。

  2. 配置线程池: 在Spring配置文件(如XML配置文件或Java配置类)中配置线程池。Spring提供了ThreadPoolTaskExecutor类,你可以使用它来配置和管理线程池。

    示例的XML配置如下:

    xml
    复制代码
    <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="5" /> <!-- 核心线程数 -->
        <property name="maxPoolSize" value="10" /> <!-- 最大线程数 -->
        <property name="queueCapacity" value="25" /> <!-- 队列容量 -->
    </bean>
    

    示例的Java配置如下:

    java
    复制代码
    @Configuration
    @EnableAsync
    public class AppConfig {
        @Bean
        public TaskExecutor taskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);
            executor.setMaxPoolSize(10);
            executor.setQueueCapacity(25);
            return executor;
        }
    }
    
  3. 使用线程池: 在你的Spring应用中,可以使用@Async注解将方法标记为异步执行,然后将方法调用委托给线程池。

    示例:

    java
    复制代码
    @Service
    public class MyService {
        @Async
        public void doSomethingAsync() {
            // 异步任务的实现
        }
    }
    

    注意:为了使@Async注解生效,需要在Spring配置中启用异步支持,如上面的Java配置中的@EnableAsync

  4. 使用线程池执行任务: 通过从ApplicationContext获取TaskExecutor(线程池)的引用,你可以手动执行异步任务。

    示例:

    java
    复制代码
    @Autowired
    private TaskExecutor taskExecutor;
    
    public void someMethod() {
        taskExecutor.execute(() -> {
            // 异步任务的实现
        });
    }