当前位置: 首页 > article >正文

JavaEE:多线程初阶

JavaEE:多线程初阶

  • 一、线程的原理和进程与线程之间的关系
    • 1. 线程的原理
      • 线程的基本概念
      • 线程的生命周期
      • 线程的调度
      • 线程的并发与并行
    • 2. 进程与线程的关系
      • 进程(Process)
      • 线程与进程的关系
      • 进程和线程的对比
      • 线程的优势
      • 线程的缺点
    • 3. 总结
  • 二、多线程的使用和 Thread 类的用法
    • 1. 使用 Thread 类来创建线程
      • 1.1 通过继承 Thread 类
      • 1.2 Thread 类常见的方法
      • 1.3 Thread 类其他方法
    • 2. 使用 Runnable 接口实现线程
      • 2.1 Runnable 接口的使用
    • 3. 线程的控制
      • 3.1 线程的休眠(sleep())
      • 3.2 线程的通知和等待(wait() ,notify() ,notifyAll())
      • 3.3 线程的中断(interrupt())
    • 4. 总结
  • 三、线程安全问题及解决方式
    • 1. 线程安全问题的产生原因
      • 1.1 竞态条件(Race Condition)
      • 1.2 可见性问题
      • 1.3 死锁(Deadlock)
    • 2. 如何解决线程安全问题
      • 2.1 使用同步(synchronized)
      • 2.2 使用 volatile 关键字
      • 2.3 使用 java.util.concurrent 包
      • 2.4. 避免死锁
    • 3. 总结
  • 四、如何理解多线程等待通知机制
    • 1. 等待通知机制的基本概念
    • 2. 如何理解等待通知机制
    • 3. 等待通知机制的使用示例
      • 3.1 生产者-消费者问题
      • 3.2 wait() 和 notify() 方法的工作机制
      • 3.3 注意事项
    • 4. wait()、notify() 和 notifyAll() 的区别
    • 5. 总结
  • 五、多线程的代码案例:单例模式
    • 1. 单例模式的应用场景
    • 2. 单例模式的实现方法
    • 3. 单例模式的优缺点
    • 4. 什么时候使用单例模式?
    • 5. 总结
  • 六、多线程的代码案例:阻塞队列
    • 1. 阻塞队列的特点
    • 2. 常用的阻塞队列实现
    • 3. 代码示例:使用 ArrayBlockingQueue 实现生产者-消费者模型
  • 七、多线程的代码案例:线程池
    • 1. 线程池的优势
    • 2. Java 中的线程池实现
    • 3. 参数说明
    • 4. 代码示例:手动模拟实现线程池执行任务
    • 5. 代码分析
  • 八、多线程的代码案例:定时器
    • 1. 使用 Timer 和 TimerTask 实现定时任务
    • 2. 使用 ScheduledExecutorService 实现定时任务
    • 3. 手动模拟实现一个定时器
    • 4. 总结

一、线程的原理和进程与线程之间的关系

在Java中,线程是执行程序中的最小单元,线程的管理和调度是由操作系统的内核和Java虚拟机(JVM)共同负责的。理解线程的原理和进程与线程的关系对于开发高效、线程安全的程序至关重要。

1. 线程的原理

线程的基本概念

线程(Thread)是计算机中的基本执行单位。 每个进程都有多个线程,每个线程都共享同一个进程中的资源(如内存,文件句柄等)。每个线程都有自己的程序计时器(Program Counter),栈(Stack)和局部变量,但它们之间都共享堆(Heap)等资源。

线程的生命周期

一个线程在其生命周期中经历以下几个状态:

  1. 新建状态(New):当线程对象被创建时,线程处于新建状态。
  2. 就绪状态(Runnable):当线程被启动并等待操作系统的调度时,它处于就绪状态。请注意,Java中的“就绪状态”是指线程已经准备好执行,但可能因为操作系统调度的原因尚未获得 CPU 时间。
  3. 运行状态(Running):当线程获得了 CPU 时间并开始执行时,线程进入运行状态。
  4. 阻塞状态(Blocked):如果线程因为等待某些资源(如 I/O 操作或锁)而无法继续执行,它将进入阻塞状态。
  5. 等待状态(Waiting):线程进入等待状态时,它正在等待其他线程的某些操作或事件(如某个线程调用 Thread.join() 或通过 Object.wait() 等方式)。
  6. 超时等待状态(Timed Waiting):线程进入超时等待状态是指它会在指定的时间内等待,常见的方法包括 Thread.sleep()Object.wait(long timeout) 等。
  7. 死亡状态(Terminated):线程在执行完毕或者因异常等原因终止时,进入死亡状态。

线程的调度

Java的线程调度都是基于线程抢占式调度的。 这意味着当前执行的线程可以被操作系统抢占并暂停,然后由操作系统调度其他线程执行。Java的线程调度器依赖于操作系统的算法调度,通常是基于优先级的调度。在大多数操作系统中。

线程的并发与并行

  • 并发指的是多个线程在同一个时段共享CPU资。即在一个时段内,多个线程同时执行。操作系统通过快速切换线程来实现并发。
  • 并行指的是多个线程在同一时刻由多个处理器或核心同时执行。对于拥有多核 CPU 的机器,操作系统可以将多个线程分配给不同的核心来实现并行。

2. 进程与线程的关系

进程(Process)

进程是程序的一次执行实例。每个进程都有自己的地址空间、数据栈和其他用于执行的辅助数据。进程是资源分配和调度的基本单位。每个进程可以包含一个或多个线程。

线程与进程的关系

  • 独立性:线程是进程中的执行单元,线程的执行不依赖于其他线程的执行。一个进程中的多个线程共享进程的资源,如内存空间、文件描述符等。
  • 资源分配:操作系统为每个进程分配资源(如内存、CPU 时间等)。而线程是共享这些资源的,多个线程可以访问相同的内存区域。
  • 创建与销毁:创建一个进程比创建一个线程更加昂贵。因为创建进程需要独立的内存空间和系统资源。而创建线程分配少量的控制资源(栈和程序计数器)。
  • 执行速度:线程的创建、销毁和上下文切换的开销比进程小得多,因此在需要大量并发任务时,线程相较于进程更为高效。

进程和线程的对比

特性进程线程
内存空间独立的内存空间共享进程的内存空间
资源分配每个进程有自己的资源多个线程共享进程的资源
创建与销毁开销创建和销毁开销较大创建和销毁开销较小
调度与切换开销进程上下文切换开销较大线程上下文切换开销较小
并发性每个进程并行执行线程并行执行,依赖于多核 CPU
异常隔离一个进程内的异常不会影响其他进程线程间的异常会影响其他线程

线程的优势

  • 线程比进程更轻量,创建和销毁的速度更快。
  • 线程之间可以共享进程的资源(如内存),从而降低了资源的消耗和管理复杂性。
  • 在多核 CPU 上,线程可以并行执行,从而充分利用硬件性能。

线程的缺点

  • 由于线程共享进程的内存空间,线程间的通信和同步比较复杂,容易引发线程安全问题。
  • 多线程程序容易出现死锁、竞态条件等并发问题,需要使用锁、条件变量等技术来保证线程安全。

3. 总结

线程是程序的执行单元,一个进程可以拥有多个线程,多个线程之间共享进程的资源。线程的创建和销毁相对比进程轻量,但也带来了一些同步和资源管理的问题。理解进程和线程之间的关系及其各自的特点,能帮助开发者更好地利用并发和并行,提高程序的性能和响应速度。

二、多线程的使用和 Thread 类的用法

在Java中,线程实现的方式有很多,用 Thread 类来实现是最直接的方式。Thread 类是 java.lang 包中的一个类,它提供了创建和控制线程的基本功能。通过继承 Thread 类或实现 Runnable 接口,可以在 Java 程序中创建和使用线程。

1. 使用 Thread 类来创建线程

1.1 通过继承 Thread 类

创建一个自定义的线程类,通过继承 Thread 类,并重写 run 方法。在 run 方法中定义线程的执行逻辑。然后,创建该类的实例,并调用 start 方法来启动线程。

示例:通过继承 Thread 类创建线程

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        // 创建并启动线程
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();

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

解释:

  • MyThread 类继承了 Thread 类,并重写了 run 方法,定义了线程的任务。
  • 调用 t1.start()t2.start() 来启动线程,这会使线程进入 就绪 状态,等待 操作系统分别 CPU 时间片来执行。

1.2 Thread 类常见的方法

  • start() :启动线程,调用 start() 方法后,操作系统会调度执行 run() 方法。每个线程只能调度一次 start() 方法。
  • run() :线程执行的任务方法,通常重写来定义线程的具体的执行逻辑。run() 方法不会直接执行,需要通过 start() 方法来启动线程。
  • sleep(long millis) :使当前线程暂停的指定的毫秒数。它会使当前线程进入 休眠 状态,等待指定时间过去,就自动恢复 就绪 状态。
  • currentThread() :返回当前线程的执行对象。
  • getName() :返回线程的名称,每个线程都有名称,默认名称是 Thread-0Thread-1 等。
  • isAlive() :判断线程是否已启动并处于活动状态。

1.3 Thread 类其他方法

  • join() :使当前线程等待调用该方法的线程执行完毕。join() 用于线程间的协调和同步。确保某个线程在执行之前,等待其他线程执行完毕。
  • interrupt() :中断线程的执行。线程会抛出 InterruptException 异常。当线程处于休眠或等待的状态,调用 Interrupt() 方法抛出该异常并终止执行。

2. 使用 Runnable 接口实现线程

虽然继承 Thread 类创建线程比较简单,但在实际开发中,通常实现 Runnable 接口来创建线程。因为 Java 是单继承的,继承 Thread 类就不能再继承其他类了,而实现 Runnable 接口的同时,还可以继承其他类。

2.1 Runnable 接口的使用

Runnable 接口只有一个方法 run() ,需要在该方法中定义线程的执行逻辑。实现 Runnable 接口时,需要创建一个 Thread 对象,并将实现了 Runnable 接口的实例传递给 Thread 构造方法。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        // 创建线程并启动
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

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

解释:

  • MyRunnable 类实现了 Runnable 接口,并重写了 run() 方法,定义了线程的任务。
  • 创建了 Runnable 对象 myRunnable,并将其传递给 Thread 的构造函数来创建线程。
  • 调用 t1.start()t2.start() 启动线程。

3. 线程的控制

3.1 线程的休眠(sleep())

Thread.sleep(long millis) 可以让当前线程暂停执行的毫秒数。在这段时间内,线程会进入休眠状态,不会参与 CPU 资源的调度,直到指定时间后才被唤醒。

示例:线程休眠

class SleepThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            try {
                // 休眠 1 秒
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "-" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo3 {
    public static void main(String[] args) {
        SleepThread sleepThread = new SleepThread();
        sleepThread.start();
    }
}

解释:

  • 每次循环,线程都会休眠 1秒(1000ms),打印当前线程的名称和循环计数。
  • 休眠期间,线程不会参与 CPU 资源的调度。

3.2 线程的通知和等待(wait() ,notify() ,notifyAll())

线程之间的协调可以通过 Object 类中的 wait()notify()notifyAll() 方法实现。通过这些方法,可以实现线程之间的通信和同步。

  • wait() 方法:使当前线程等待,直到被其他线程唤醒。
  • notify() 方法:唤醒一个正在等待的线程。
  • notifyAll() 方法:唤醒所有正在等待的线程。

这些方法需要在同步代码块中使用,与 synchronized 关键字一起使用,确保线程安全。

3.3 线程的中断(interrupt())

通过 Thread.interrupt() 方法可以请求线程线程中断。通常,线程在执行过程中会检查自身的中断状态,并决定是否停止执行。中断通常用于需要及时停止长时间运行的任务。

示例:线程中断

class InterruptThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (Thread.interrupted()) {
                System.out.println(Thread.currentThread().getName() + "was interrupt");
                break;
            }
            System.out.println(Thread.currentThread().getName() + "-" + i);
        }
    }
}
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        InterruptThread interruptThread = new InterruptThread();
        interruptThread.start();

        Thread.sleep(1000);
        // 中断线程
        interruptThread.interrupt();
    }
}

解释:

  • Thread.interrupted() :检查线程是否已经终止。
  • 调用 interrupt() 后,线程会在执行过程中通过检查 interrupted() 方法决定是否终止。

4. 总结

  • Thread 类提供了直接创建和控制线程的方法,包括 start()sleep()join()interrupt() 等。
  • 使用 Runnable 接口可以避免继承 Thread 带来的局限,允许一个类实现多个接口并同时控制线程的执行。
  • 线程的执行、控制、等待和中断都可以通过相应的方法进行管理,以实现高效的并发编程。

三、线程安全问题及解决方式

在Java中,线程安全指多个线程在并发执行的情况下,能够正确地访问和操作共享资源,不会导致数据不一致,出现异常或程序奔溃。线程安全问题通常出现在多个线程同时访问或修改共享资源时,尤其是当资源的状态依赖于多个操作的组合时。为了保证线程安全问题,确保线程间对共享资源的访问是有序的,避免竞争条件和死锁等问题。

1. 线程安全问题的产生原因

1.1 竞态条件(Race Condition)

竞态条件是多线程程序中一个常见的问题。当多个线程并发访问或修改共享资源时,如果线程间的执行顺序不确定时,可能会导致不正确的结果。例如多个线程同时读取,修改共享资源,如果没有同步机制来保证数据的一致性,就会出现竞态条件。

示例:竞态条件

class Counter {
    private static int count = 0;

    public void increment() {
        count++; // 非原子的操作
    }

    public int getCount() {
        return count;
    }
}
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程,分别执行 increment 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

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

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

        // 理论上是 10w 的
        System.out.println("count: " + counter.getCount()); // count: 53131
    }
}

解释:
在这个例子中, counter.increment() 方法不是一个原子操作,它包括读取 count (在内存中),对 count 进行 +1 操作(在寄存器中),然后再读回 count 中(内存中)。如果两个线程同时执行这个操作,就可能会发生竞态条件,导致预期的值小于 10w 。例如两个线程都读取了相同的 count 值,增加后写回。

1.2 可见性问题

当多个线程访问共享资源时,其中一个线程的修改,可能不会立即对其他线程可见。这是因为线程会缓存变量的值或写入寄存器中,导致其他线程没法看到最新的值。

示例:可见性问题

class SharedData {
    private static boolean flag = false;

    // 写操作
    public void write() {
        flag = true;
    }

    // 读操作
    public void read() {
        if (flag) {
            System.out.println("Flag is true"); // Flag is true
        } else {
            System.out.println("Flag is false");
        }
    }
}
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        SharedData data = new SharedData();

        Thread t1 = new Thread(data::write);
        Thread t2 = new Thread(data::read);

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

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

解释:

  • 在这个例子中,可能 t2 线程在 t1 线程完成 write() 操作时,就已经执行 read() 操作,就会导致 t2 线程看到 flag 还是久值,而并不是新值。这个问题时线程缓存和优化导致不可见问题。

1.3 死锁(Deadlock)

死锁发生在多个线程因相互等待对方持有的资源而无法继续执行时。死锁会导致线程无法完成任务,从而使整个程序陷入僵局。

示例:死锁

class A {
    synchronized void methodB(B b) {
        System.out.println("Thread 1: Locking A and calling B"); // 锁定 A 并呼叫 B
        b.last(); // 尝试获取 B 中的锁
    }
    synchronized void last() {
        System.out.println("In A's last method");
    }
}
class B {
    synchronized void methodA(A a) {
        System.out.println("Thread 2: Locking B and calling A"); // 锁定 B 并呼叫 A
        a.last(); // 尝试获取 A 中的锁
    }
    synchronized void last() {
        System.out.println("In B's last method");
    }
}
public class Demo7 {
    public static void main(String[] args) {
        final A a = new A();
        final B b = new B();

        Thread t1 = new Thread(() -> {
            a.methodB(b);
        });
        Thread t2 = new Thread(() -> {
            b.methodA(a);
        });

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

解释:

  • 在这个示例中,t1 线程获取 A 对象的锁并尝试获取 B 对象的锁,而 t2 线程获取 B 对象的锁,并尝试获取 A 线程对象的锁。两个线程都在等待对方释放锁,导致死锁,程序无法继续执行。

2. 如何解决线程安全问题

2.1 使用同步(synchronized)

Java提供了 synchronized 关键字,确保只有一个线程能够访问某个共享资源。通过方法或代码块上使用 synchronized 关键字,可以防止多个线程在执行同一段代码时,从而避免竞态条件。

示例:使用 synchronized 防止竞态条件

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;  // 原子操作
    }

    public int getCount() {
        return count;
    }
}

解释:

  • increment() 方法上使用 synchronized 可以确保在同一时刻只有一个线程可以执行此方法,避免了竞态条件。

2.2 使用 volatile 关键字

volatile 关键字用于保证变量在多个线程间的可见性。当一个线程修改了 volatile 变量时,其他线程能够立即看到这个修改,而不必等到线程退出同步块。

示例:使用 volatile 保证可见性

class SharedData {
    private volatile boolean flag = false;

    public void write() {
        flag = true;
    }

    public void read() {
        if (flag) {
            System.out.println("Flag is true");
        } else {
            System.out.println("Flag is false");
        }
    }
}

解释:

  • 使用 volatile 关键字,确保 flag 变量的值对于所有线程都是可见的,避免了可见性问题。

2.3 使用 java.util.concurrent 包

Java 提供了并发工具类,这些类在设计的时候考虑到了线程安全问题,并且能够更加简洁的,可靠的解决线程安全问题。

  • ReentrantLock :比 synchronized 关键字提供的锁定机制更加灵活,例如可以中断锁或定时锁。
  • Atomic 类:AtomicIntegerAtomicBoolean 等类提供对基本数据类型的原子操作,确保并发访问不会出现竞态条件。
  • CountDownLatchCyclicBarrier 等同步工具类:它们提供了线程间的协调,确保一定的执行顺序。

示例:使用 AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        // count++;
        count.incrementAndGet();  // 原子操作
    }

    public int getCount() {
        return count.get();
    }
}

解释:
AtomicInteger 提供了对整数类型的原子操作,确保在多线程条件下对 count 的增减操作是线程安全的。

2.4. 避免死锁

  • 避免嵌套锁:避免多个线程持有多个锁,避免多个线程相互等待。
  • 使用锁的顺序:多个线程请求锁时,始终按照相同的顺序获取锁,避免循环等待。
  • 使用 tryLockReentrantLock 中的 tryLock 方法尝试获取锁时,可以避免死锁。

3. 总结

线程安全问题包括竞态条件,可见性问题和死锁等问题,为了解决这些问题,Java 提供了一下几种同步机制。

  • synchronized 关键字:为了解决线程同步性问题。
  • volatile 关键字:为了解决线程可见性问题。
  • 使用 java.util.concurrent 包中的工具类(如 Atomic 类和 ReentrantLock ):来简化线程安全问题。
  • 设计时要尽量避免死锁,通过适当的锁管理和资源访问顺序来避免死锁的发生。

四、如何理解多线程等待通知机制

在 Java 中,等待通知机制(Wait-Notify Mechanism)是多线程编程中的一种重要技术,用于协调多个多个线程的执行顺序。当多个线程在并发环境下操作共享资源时,某些线程只能等待其他线程完成任务时才能继续执行。 通过 Object 类中的 waitnotifynotifyAll 等方法,Java 提供了线程间的协调机制。

1. 等待通知机制的基本概念

等待通知机制本质上就是通过共享对象同步线程的执行状态。某些线程可能需要满足条件才能继续执行(等待),而其他线程在满足条件后唤醒等待的线程(通知)。这种机制通常解决生产者-消费者模型等场景。

  • wait() :让当前线程进入等待状态,直到其他线程使用 notifynotifyAll 唤醒当前线程。该方法必须在同步代码块中使用(synchronized)。
  • notify() :唤醒一个正在等待该对象锁的线程。被唤醒的线程再次尝试获取锁,继续执行。
  • notifyAll() :唤醒所有正在等待该对象锁的线程。所有被唤醒的线程再次尝试获取锁,哪个线程获取到锁对象,哪个线程就继续执行。

2. 如何理解等待通知机制

  1. 线程和锁:线程和对象是紧密相关的,线程只有获取到锁对象才能执行同步代码块(即 synchronized 块或同步方法)。否则阻塞等待,直到获取到锁对象才能继续执行。
  2. 等待和通知的关系:当一个线程执行到 wait() 时,就会释放锁并进入等待队列,直到其他线程调用 notify()notifyAll()notify()notifyAll() 唤醒的是等待该锁对象的线程,并且这些线程只有获取到锁才能够继续执行。
  3. 条件变量:等待通知机制核心思想是根据某种条件决定是否继续执行。通常,线程会在一个条件变量上等待,该条件变量表示某种资源已经准备好。例如,生产者线程等待缓冲区有空位,消费者线程等待缓冲区有商品。
  4. 单个线程和多个线程:使用 notify 唤醒一个正在等待的线程,而 notifyAll 则唤醒所有等待的线程,但是,不是所有线程都被唤醒,只有它们在获取到锁的时候才能继续执行。

3. 等待通知机制的使用示例

3.1 生产者-消费者问题

这是等待通知机制的经典案例。假设有一个缓冲区,生产者线程将写入数据到缓冲区,而消费者线程则从缓冲区拿数据。生产者线程需要等到缓冲区中有空位才能写入数据,而消费者线程需要等到缓冲区中有数据才能消费。

示例:生产者-消费者模型

class SharedBuffer {
    private int[] buffer = new int[10]; // 缓冲区
    private int count = 0; // 当前缓冲区的数据量

    // 生产者线程
    public synchronized void produce(int value) throws InterruptedException {
        // 缓冲区数据量满了,生产者线程就阻塞等待
        if (count == buffer.length) {
            wait();
        }
        // 向缓冲区添加数据
        buffer[count++] = value;
        System.out.println("produce: " + value);
        // 唤醒消费者线程
        notify();
    }

    // 消费者线程
    public synchronized int consumer() throws InterruptedException {
        // 缓冲区数据量为 0,等待生产者生产
        if (count == 0) {
            wait();
        }
        // 从缓冲区取出数据
        int value = buffer[--count];
        System.out.println("consumer: " + value);
        // 唤醒生产者线程
        notify();
        return value;
    }
}
public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        SharedBuffer buffer = new SharedBuffer();
        // 生产者-消费者模型,需要两个线程来实现

        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    buffer.produce(i);
                    Thread.sleep(1000); // 模拟生产时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 消费者线程
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 150; i++) {
                try {
                    buffer.consumer();
                    Thread.sleep(1500); // 模拟消费时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

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

解释:

  • SharedBuffer 类中包含一个缓冲区和一个计数器 count,用来记录缓冲区中的元素个数。
  • 生产者线程在缓冲区满时时调用 wait() ,等待消费者线程消费数据,而消费者线程在缓冲区空时,会调用 wait() 方法,等待生产者线程生产数据。
  • 每当生产者线程生产数据时会调用 notify() ,唤醒消费者消费数据,而当消费者线程消费数据时也会调用 notify() ,也会唤醒生产者线程生产数据。
  • wait()notify() 必须在 synchronized 方法或代码块中使用,因为它们依赖于对象的锁。

3.2 wait() 和 notify() 方法的工作机制

  • wait() 方法使当前线程进入等待队列,释放当前持有的锁,直到其它线程唤醒。调用 wait() 方法,不会继续执行下去,直到被等待队列解除。
  • notify() 方法从等待队列唤醒唤醒一个线程,并使其重新获得锁对象。一旦获得锁对象,就会继续执行下去。
  • notifyAll() 方法唤醒所有在当前监视器(锁)等待的线程,所有被唤醒的锁同时竞争锁。

3.3 注意事项

  • wait() 和 notify() 必须在同步方法或同步代码块中使用,因为它们依赖于锁的机制,假如没有同步代码块的保障,它们的行为无法被预测。
  • notify() 唤醒的线程并不意味着立即执行,它只是将线程从等待队列中移除,线程会再次竞争锁资源。被唤醒的线程只有先获取到锁,才能继续执行。
  • 线程的顺序不可预测:使用 notify() 或 notifyAll() 时,被唤醒的线程获得锁的顺序是由 JVM 和操作系统决定的。因此避免依赖于线程的唤醒顺序。

4. wait()、notify() 和 notifyAll() 的区别

  • wait() :让当前线程进入等待状态,直到被其他线程通知唤醒。释放锁并进入等待队列。
  • notify() :唤醒一个在该对象锁等待的线程。
  • notifyAll() :唤醒所有在该对象锁等待的线程。

notify() 和 notifyAll() 的选择:

  • notify() :适用于一个线程状态改变足以唤醒另一个线程。例如生产者-消费者模型,一个生产元素,一个就消费元素。
  • notifyAll() :适用于多个线程等待同一个条件下,尤其是在多线程相互依赖的场景下。尤其是在多线程相互依赖的场景下。

5. 总结

等待通知机制在 Java 中是一种强大的线程同步手段,用于解决多线程协调的问题。通过 wait()notify()notifyAll() 方法,可以在线程之间传递控制信号,保证线程按照特定顺序执行。它通常用于处理如生产者-消费者问题、线程池的工作队列等场景。

五、多线程的代码案例:单例模式

1. 单例模式的应用场景

单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。在多线程环境下,单例模式的应用场景包括:

  • 配置管理:确保配置信息只有一个实例,方便管理和访问。
  • 日志记录:确保日志记录器只有一个实例,避免日志混乱。
  • 数据库连接池:管理数据库连接,确保连接池只有一个实例,提高效率。
  • 缓存:确保缓存只有一个实例,方便数据共享和一致性维护。

2. 单例模式的实现方法

在Java中,单例模式有多种实现方法,特别是在多线程环境下,需要考虑线程安全的问题。常见的实现方法包括:

  • 懒汉式(线程不安全)

    public class Singleton {
        private static Singleton instance;
    
        private Singleton() {}
    
        public Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    这种实现方式在多线程环境下是不安全的,因为多个线程可能同时进入if (instance == null)判断,导致创建多个实例。

  • 懒汉式(线程安全)

    public class Singleton {
        private static Singleton instance;
    
        private Singleton() {}
    
        public synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    通过在getInstance()方法上添加synchronized关键字,确保线程安全,但会导致性能下降,因为每次调用getInstance()都会进行同步。

  • 饿汉式(Eager Initialization)

    public class Singleton {
        private static final Singleton instance = new Singleton();
        //构造方法:防止 new 一个对象实例化对象
        private Singleton() {}
        //获取到一个Singleton
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    这种实现方式在类加载时就创建实例,确保线程安全,但可能会提前占用资源。

  • 静态内部类(Static Inner Class)

    public class Singleton {
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton instance = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.instance;
        }
    }
    

    这种实现方式利用类加载机制确保线程安全,且延迟加载,性能较好。

  • 双重检查锁(Double-Checked Locking)

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}
    
        public Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    解释:

    1. volatile 关键字:实例变量为 private static volatile Singleton instance; volatile 关键字确保该变量的更改对所有线程都是可见的,防止与缓存或指令重排序等问题。
    2. getInstance() 方法:
    • 第一次检查:该方法首先在不获取锁的情况下检查 instance 是否为 null 。如果不为 null ,就直接返回现有实例,避免进入同步代码块。
    • 同步同步块:如果 instancenull 时,就进入 Singleton.class 同步代码块。确保只有一个线程可以继续创建实例。
    • 第二次检查:在同步代码块内,再次判断 instance 是否为 null ,这是为了防止其他线程创建完实例后再次创建实例。

3. 单例模式的优缺点

优点:

  • 全局唯一实例:确保一个类只有一个实例,避免资源浪费。
  • 控制资源访问:通过单例实例控制对资源的访问,方便管理。
  • 简化对象管理:不需要关心对象的创建和销毁,只需要通过全局访问点获取实例。

缺点:

  • 隐藏的通信机制:单例模式可能隐藏了对象之间的通信机制,增加系统复杂性。
  • 测试困难:单例模式可能导致测试困难,因为实例是全局的,难以进行单元测试。
  • 潜在的性能瓶颈:如果单例类成为性能瓶颈,可能需要重新设计系统。

4. 什么时候使用单例模式?

  • 当系统中某个类只需要一个实例,而且客户端可以随时获取该实例时。
  • 当需要全局访问某个对象,并且该对象的状态需要保持一致时。
  • 当需要控制资源的使用,例如数据库连接池、线程池等。
  • 当某个类的实例化成本较高,需要复用该实例时。

5. 总结

单例模式是一种常用的设计模式,确保一个类只有一个实例,并提供一个全局访问点。在多线程环境下,需要特别注意线程安全问题,选择合适的实现方法。常见的实现方法包括懒汉式、饿汉式、双重检查锁和静态内部类等。根据具体需求和场景选择合适的单例模式实现方式,可以有效提高系统的性能和可靠性。

六、多线程的代码案例:阻塞队列

在 Java 多线程编程中,阻塞队列(BlockingQueue) 是一种线程安全的数据结构。常用于实现生产者-消费者模型。

1. 阻塞队列的特点

  • 线程安全:阻塞队列内部实现了线程同步,确保了多个线程可以安全地进行入队列和出队列操作。
  • 阻塞特征:当队列已经满了,再试图添加元素到队列中就会发生阻塞,直到有空间可用。当队列为空时,尝试从队列中取出元素,就会发生阻塞,直到队列中直到有新的元素添加。

2. 常用的阻塞队列实现

  • ArrayBlockingQueue :基于数组的有界阻塞队列,必须指定容量。
  • LinkedBlockingQueue :基于链表的阻塞队列,默认情况下容量为 Integer.MAX_VALUE ,也可以指定容量。
  • PriorityBlockingQueue :支持元素优先级的无界阻塞队列。

3. 代码示例:使用 ArrayBlockingQueue 实现生产者-消费者模型

代码示例:

public class Demo10 {
    private static final int QUEUE_CAPACITY = 5; // 阻塞队列中的容纳的量
    private static final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(QUEUE_CAPACITY ); // 阻塞队列

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

    // 生产者线程
    static class producer implements Runnable {
        @Override
        public void run() {
            int value = 0; // 数据量
            try {
                while (true) {
                    System.out.println("生产者准备生产数据~");
                    queue.put(value);
                    System.out.println("生产者生产数据:" + value);
                    value++;
                    Thread.sleep(1000); // 模拟生产过程中的耗时操作
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // 消费者线程
    static class consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println("消费者准备消费数据~");
                    Integer value = queue.take();
                    System.out.println("消费者消费数据:" + value);
                    Thread.sleep(1500); // 模拟消费过程中耗时操作
                }
            } catch (InterruptedException e) {

            }
        }
    }
}

代码分析:

  1. 阻塞队列的初始化ArrayBlockingQueue 被初始化为固定容量的队列,用于生产者消费者之间的传递。
  2. 生产者线程:不断的生产整数,添加到队列中。使用 put() 方法在队列满时,就会产生阻塞,直到有空间可用。
  3. 消费者线程:不断地从队列中取出整数并处理。使用 take() 方法时在队列空时,也会产生阻塞,直到有元素在对队列中出现。
  4. 线程休眠Thread.sleep 模拟线程生产和消费地耗时操作。使生产和消费的速度不同,体现出阻塞队列调节作用。

通过阻塞队列,生产者和消费者可以在不同步的情况下进行协作,生产者无需等待消费者处理完毕即可继续生产,消费者也无需等待生产者提供数据即可继续消费,从而提高系统的并发性能和资源利用率。

此外,阻塞队列在实现生产者-消费者模型时,具有以下优势:

  • 解耦合: 生产者和消费者通过阻塞队列来进行通信,彼此不直接产生依赖,降低系统的耦合度。
  • 削峰填谷: 在高并发的场景下,阻塞队列充当缓冲区,平衡生产者和消费者的速度,避免系统过载。

七、多线程的代码案例:线程池

在 Java 多线程编程中,线程池是一种用于管理和复用线程的机制。能够有效地控制并发线程数量,降低系统消耗,提升执行效率。

1. 线程池的优势

  • 降低资源消耗: 通过重复利用已创建的线程,减少频繁的创建和销毁线程所带来的开销。
  • 增强响应速度: 当任务达到时,无需等待新线程的创建即可执行,提高系统的响应性。
  • 提高可管理性: 线程池统一分配、调优和监控线程,避免线程耗尽或过载,提高系统稳定性。

2. Java 中的线程池实现

Java 提供了 java.util.concurrent 包来支持线程池功能,其中核心类是 ThreadPoolExecutor

ThreadPoolExecutor 的构造方法

public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程的存活时间
    TimeUnit unit,           // 存活时间的单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
) {
    // 构造函数实现
}

3. 参数说明

  • corePoolSize :核心线程数,线程池在空闲的时间仍保存的线程数量。
  • maximumPoolSize :最大线程数,线程池所能够创建的最大线程数量。
  • keepAliveTimeunit :当线程数超过核心线程数时,多余的空闲线程会在终止前等待新任务的最长时间。
  • workQueue :用于保存等待执行任务的阻塞队列,常用的阻塞队列有:
    • ArrayBlockingQueue :基于数组的有界阻塞队列,按 FIFO(先进先出)顺序对元素进行排序。
    • LinkedBlockingQueue :基于链表的阻塞队列,吞吐量通常高于 ArrayBlockingQueue
    • SynchronousQueue :一个不存储元素的阻塞队列,执行插入操作之前,需要阻塞等待相应地删除操作。
  • threadFactory :关于创建新线程的工厂,可用于为每个创建的线程设置名称和优先级等属性。
  • handler :当线程池和队列都满时,执行拒绝策略操作。常见的拒绝策略:
    • AbortPolicy :直接抛出异常,默认策略。
    • CallerRunsPolicy :由调用线程执行。
    • DiscardOldestPolicy :丢弃队列中最老的任务,尝试执行当前线程。
    • DiscardPolicy :直接丢弃当前线程。

4. 代码示例:手动模拟实现线程池执行任务

import java.util.concurrent.ArrayBlockingQueue;

// 实现一个固定线程个数的线程池
class MyThreadPool {
    //阻塞队列
    private ArrayBlockingQueue<Runnable> queue = null;

    public MyThreadPool(int n) {
        // 初始化线程,固定数目的线程数
        // 以 ArrayBlockingQueue 作为任务队列,固定容量为 1000
        queue = new ArrayBlockingQueue<>(1000);

        // 创建 N 个线程
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        Runnable task = queue.take();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
    //提交任务
    public void submit(Runnable task) throws InterruptedException {
        // 把任务丢到队列中
        queue.put(task);
    }

}
public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool threadPool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int id = i;
            threadPool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " id=" + id);
            });
        }
    }
}

5. 代码分析

首先,MyThreadPool类定义了一个私有的ArrayBlockingQueue<Runnable>类型的成员变量queue,初始值为null

在构造函数public MyThreadPool(int n)中,执行了以下操作:

  1. 初始化线程池,固定线程数为n

  2. 使用ArrayBlockingQueue作为任务队列,固定容量为1000。

  3. 创建n个线程,每个线程通过一个Thread对象来表示。

  4. 在线程的运行代码中,使用try-catch块来处理可能的InterruptedException

  5. try块中,使用while (true)循环来不断从队列中获取任务并执行。

  6. 如果获取任务成功,调用task.run()来执行任务。

  7. 如果在等待任务时被中断,捕获InterruptedException并打印堆栈跟踪信息。

接着,MyThreadPool类定义了一个公共方法public void submit(Runnable task) throws InterruptedException,用于提交任务到线程池。

  1. 这个方法将任务添加到阻塞队列中,使用queue.put(task)

  2. 如果队列已满,调用put方法将阻塞,直到有空间可用。

在主类Demo11中,定义了一个main方法,用于演示如何使用MyThreadPool

  1. 创建一个线程池,指定线程数为10。

  2. 使用for循环提交100个任务到线程池。

  3. 每个任务是一个Runnable,其运行时打印当前线程的名称。

需要注意的是,由于线程池的线程数是10,而任务数是100,所以任务会被依次添加到阻塞队列中,然后由这10个线程来处理。

这个自定义的线程池实现相对简单,没有涉及线程的回收、线程工厂等更复杂的功能。在实际应用中,通常会使用Java提供的ExecutorServiceThreadPoolExecutor等类来实现更 robust 的线程池管理。

另外,代码中使用了lambda表达式来定义Runnable任务,这在Java 8及以后的版本中是可用的。如果使用更早的Java版本,可能需要使用匿名内部类来实现Runnable接口。

八、多线程的代码案例:定时器

在 Java 中,定时任务的实现主要依赖于 Timer 类和 ScheduledExecutorService 接口。

1. 使用 Timer 和 TimerTask 实现定时任务

Timer 时Java提供的定时器类,主要用于在指定时间安排任务的执行。

代码示例:

import java.util.Timer;
import java.util.TimerTask;

public class Demo12 {
    public static void main(String[] args) {
        Timer timer = new Timer();

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务执行,时间:" + System.currentTimeMillis());
            }
        };

        // 延迟 1 秒后执行,每 3 秒执行一次
        timer.scheduleAtFixedRate(task, 1000, 3000);
    }
}

代码分析:

  • Timer 对象:用于安排任务。
  • TimerTask :需要继承并实现 run 方法,定义任务的具体内容。
  • scheduleAtFixedRate 方法:第一个参数是要执行的任务,第二参数是初始延迟的时间(单位为毫秒),第三个参数是执行间隔(单位为毫秒)。

注意事项:

  • Timer 使用单一后台线程执行任务,如果某个任务执行时间过长,就会影响其他线程的执行。
  • Timer 不适合处理需要精细控制的并发任务。

2. 使用 ScheduledExecutorService 实现定时任务

ScheduledExecutorService 是 Java5 引入的定时任务调度器,提供了更加灵活和线程安全的定时任务功能。

示例代码:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo13 {
    public static void main(String[] args) {
        ScheduledExecutorService schedule = Executors.newScheduledThreadPool(2);

        Runnable task = () -> {
            System.out.println("任务执行,时间:" + System.currentTimeMillis());
        };
        // 延后 1 秒执行任务,每 3 秒执行一次
        schedule.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
    }
}

代码分析:

  • ScheduledExecutorService 对象:通过 newScheduledThreadPool 方法来创建,参数是线程池的数量。
  • Runnable 接口:使用 Lambda 表达式来定义任务内容。
  • scheduleAtFixedRate 方法:第一个参数是要执行的内容,第二个参数是初始延迟,第三个参数是执行间隔,第四个参数是时间单位。

优势:

  • ScheduledExecutorService 使用线程池,可以同时执行多个任务,避免单线程。
  • 提供更好的异常处理和任务调度能力。

3. 手动模拟实现一个定时器

代码示例:

import java.util.PriorityQueue;

// 写一个TimerTask
class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable task; // 队列中要执行的任务
    private long time; // 记录任务要执行的时刻

    public MyTimerTask(Runnable task, long time) {
        this.task = task;
        this.time = time;
    }

    //优先级队列要比较的顺序
    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
    }

    public long getTime() {
        return time;
    }

    public void run() {
        task.run(); // Runnable task
    }
}

// 写一个定时器
class MyTimer {
    // 1. 需要一个数据结构来管理队列中的任务(优先级队列)
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 2. 多个线程操作一个队列,势必会存在线程安全问题(当前调用 schedule 把任务添加到队列中,还有一个线程执行队列中的任务)
    private Object locker = new Object();

    // 3. 实现一个 schedule 方法把任务添加到队列中
    public void schedule(Runnable task, long delay) {
        synchronized (locker) {
            // 以入队列这个时刻作为时间基准
            MyTimerTask myTimerTask = new MyTimerTask(task, System.currentTimeMillis() + delay); // 计算出任务的执行时间(系统时间+延迟时间)
            queue.offer(myTimerTask);
            // 在执行 schedule 方法时唤醒 wait
            locker.notify();
        }
    }

    // 4. 构造方法:创建一个线程,来执行队列中的任务
    public MyTimer() {
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    synchronized (locker) {
                        // 取出栈顶元素
                        while (queue.isEmpty()) {
                            // continue;
                            locker.wait(); // 与 while 搭配在一起使用
                        }
                        MyTimerTask task = queue.peek();
                        if (System.currentTimeMillis() < task.getTime()) {
                            // 还没到时间
                            // continue;
                            locker.wait(task.getTime() - System.currentTimeMillis()); // 解决忙等问题
                        } else {
                            task.run(); // 执行队列中的任务
                            queue.poll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        });
        // 启动线程
        t.start();
    }
}

public class Demo4 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        }, 3000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        }, 2000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        }, 1000);
    }
}

代码分析:
这段代码实现了一个简单的定时器类 MyTimer,它使用优先队列(PriorityQueue)来管理任务,并在一个后台线程中执行这些任务。

类 MyTimerTask

MyTimerTask 实现了 Comparable<MyTimerTask> 接口,以便可以将任务按照执行时间排序。

  • 成员变量:

    • task: 一个 Runnable 对象,表示要执行的任务。
    • time: 表示任务应该被执行的时间戳(以毫秒为单位)。
  • 构造方法:

    • MyTimerTask(Runnable task, long time): 初始化任务和执行时间。
  • 方法:

    • compareTo(MyTimerTask o): 比较两个任务的执行时间,确保优先队列中的任务按时间顺序排列。

      @Override
      public int compareTo(MyTimerTask o) {
          return Long.compare(this.time, o.time);
      }
      

      注意:原代码中的 (int) (this.time - o.time) 可能会导致整数溢出问题,建议使用 Long.compare 方法。

    • getTime(): 返回任务的执行时间。

    • run(): 执行任务。

类 MyTimer

MyTimer 类负责管理任务队列,并在适当的时间执行任务。

  • 成员变量:

    • queue: 一个优先队列,用于存储 MyTimerTask 对象。
    • locker: 一个锁对象,用于同步访问共享资源。
  • 方法:

    • schedule(Runnable task, long delay): 安排一个任务在指定的延迟后执行。

      public void schedule(Runnable task, long delay) {
          synchronized (locker) {
              MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
              queue.offer(timerTask);
              locker.notify();
          }
      }
      
    • 构造方法 MyTimer():
      创建并启动一个后台线程,该线程负责从优先队列中取出任务并在适当的时间执行它们。

      public MyTimer() {
          Thread t = new Thread(() -> {
              try {
                  while (true) {
                      synchronized (locker) {
                          while (queue.isEmpty()) {
                              locker.wait();
                          }
                          MyTimerTask task = queue.peek();
                          if (System.currentTimeMillis() < task.getTime()) {
                              locker.wait(task.getTime() - System.currentTimeMillis());
                          } else {
                              task.run();
                              queue.poll();
                          }
                      }
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          });
          t.start();
      }
      

代码改进点

  1. compareTo 方法:
    使用 Long.compare 方法代替 (int) (this.time - o.time),避免整数溢出问题。

    @Override
    public int compareTo(MyTimerTask o) {
        return Long.compare(this.time, o.time);
    }
    
  2. 线程安全:
    确保所有对共享资源(如 queue)的操作都在同步块内进行。

  3. 异常处理:
    在实际应用中,可能需要更健壮的异常处理机制,例如重新抛出异常或记录日志。

  4. 线程中断:
    当前实现没有提供停止后台线程的方法。如果需要停止定时器,可以考虑添加一个标志位或其他机制来安全地终止线程。

4. 总结

对于简单的定时任务,可以使用 TimerTimerTask

对于复杂的并发定时任务,推荐使用 ScheduledExecutorService,以获得更高的灵活性和可靠性。

在实际应用中,应根据具体需求选择合适的定时任务实现方式,确保程序的效率和稳定性。


http://www.kler.cn/a/507024.html

相关文章:

  • 仿射密码实验——Python实现(完整解析版)
  • 【Qt】03-页面切换
  • SpringMVC (1)
  • 手摸手实战前端项目CI CD
  • 【漏洞分析】DDOS攻防分析
  • SpringBoot入门实现简单增删改查
  • shell安全类脚本(1.屏蔽每分钟访问过多的IP;2.拒绝ssh暴力破解)
  • MySQL基本知识梳理
  • linux上使用update-alternatives来选择软件版本
  • Jenkins+Docker一键打包部署项目!步骤齐全,少走坑路!
  • Vue3中使用组合式API通过路由传值详解
  • 模型参考自适应控制算法介绍及代码例程
  • 【机器学习:十八、更高级的神经网络概念】
  • Fiddler、Charles、Wireshark 和 Sniffmaster 工具对比
  • vscode【实用插件】Material Icon Theme 美化文件图标
  • 大疆发布可折叠航拍无人机,仅重249g,支持 4800 万像素拍摄
  • vue3+js使用elementplus的ElMessage弹窗报错:ElMessage‘ is not defined.eslintno-undef
  • mybatis的多对一、一对多的用法
  • Git在码云上的使用指南:从安装到推送远程仓库
  • 每日进步一点点(网安)
  • Spring Boot 统一返回数据格式
  • 【2025最新版】PCL点云处理算法汇总(C++长期更新版)
  • 从零深度学习:(2)最小二乘法
  • 网安——CSS
  • [Linux]——进程(2)
  • “AI智能服务平台系统,让生活更便捷、更智能