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

Java 多线程技术详解

文章目录

  • Java 多线程技术详解
    • 目录
    • 引言
    • 多线程的概念
      • 为什么使用多线程?
      • 多线程的特征
      • 多线程的挑战
    • 多线程的实现方式
      • 3.1 继承 `Thread` 类
        • 示例代码:
      • 3.2 实现 `Runnable` 接口
        • 示例代码:
      • 3.3 使用 `Executor` 框架
        • 示例代码:
      • 3.4 使用 `Callable` 和 `Future`
        • 示例代码:
    • 线程的生命周期
    • 线程状态
      • 新建状态(New)
      • 就绪状态(Runnable)
      • 运行状态(Running)
      • 阻塞状态(Blocked / Waiting / Timed Waiting)
      • 终止状态(Terminated)
      • 线程状态转换
    • 线程调度
      • 调度策略
      • Java线程调度
      • 影响线程调度的因素
      • 调度的不可预测性
    • 线程安全
      • `synchronized`关键字的使用
      • `synchronized`关键字的特点
      • `synchronized`的局限性
      • 如何优化`synchronized`
      • 示例代码
      • 7.2 Lock 接口
        • ReentrantLock 类
        • Condition 接口
        • 示例代码
        • 详细解释
          • 构造函数
          • put() 方法
          • take() 方法
          • 使用`awaitNanos()`方法的原因
        • 总结
    • 线程间通信
      • 线程间通信概述
      • Java 中的线程间通信
        • 使用`Object`的`wait()`和`notify()`方法
        • 使用`Lock`接口和`Condition`接口
      • 示例代码
      • 详细解释
        • 构造函数
        • put() 方法
        • take() 方法
      • 使用`awaitNanos()`方法的原因
      • 总结
    • 避免死锁
      • 死锁的四个必要条件
      • 如何避免死锁
        • 1. **破坏互斥条件**
        • 2. **破坏占有并等待条件**
        • 3. **破坏非抢占条件**
        • 4. **破坏循环等待条件**
    • 线程池
      • 10.1 ExecutorService
      • 线程池的基本概念
      • Java中的线程池
      • 创建线程池
        • 线程池参数
        • 示例代码
      • 1. Fixed Thread Pool
      • 2. Cached Thread Pool
      • 3. Scheduled Thread Pool
      • 4. Single Thread Executor
    • 线程中断
      • 如何中断一个线程
      • 检查线程中断状态
      • 示例代码
      • 注意事项
    • 守护线程
      • 守护线程的特点
      • 创建守护线程
      • 示例代码
      • 注意事项
    • 线程组
      • 线程组的作用
      • 创建线程组
      • 示例代码
      • 线程组的方法
      • 注意事项
    • 线程本地存储
      • 什么是线程本地存储?
      • `ThreadLocal`类的使用
      • 创建`ThreadLocal`实例
      • 示例代码
      • `ThreadLocal`类的方法
      • 注意事项
    • 总结
      • Java多线程的核心概念
      • 实现多线程的方式
      • 线程间通信
      • 使用`Object`的`wait()`和`notify()`方法
      • 使用`Lock`接口和`Condition`接口
      • 避免死锁
      • 线程池
      • 线程中断
      • 守护线程
      • 线程组
      • 线程本地存储

Java 多线程技术详解

目录

引言

Java多线程是Java平台的一个核心特性,它为开发人员提供了一种在单个程序中同时执行多个任务的能力。这种并发执行的能力不仅极大地提高了程序的执行效率,还允许软件更好地利用现代多核处理器的硬件资源,从而实现高性能和高响应性的应用。

多线程编程的核心优势在于它能够实现任务的并行处理,即在同一时间处理多个任务,这对于处理I/O密集型或计算密集型的工作负载特别有效。例如,一个服务器应用程序可以同时处理多个客户端请求,或者一个数据分析程序可以在不同的数据集上并行运行算法。

然而,多线程编程也带来了复杂性,特别是当涉及到线程之间的数据共享和同步时。如果不恰当地管理,多线程程序可能会遭受竞态条件、死锁、活锁、饥饿和资源泄露等问题。因此,理解线程生命周期、线程状态、线程调度、线程安全、线程间通信以及如何有效地使用线程池和其他高级同步机制,对于成功开发多线程应用程序至关重要。

Java标准库提供了丰富的API来支持多线程编程,包括Thread类、RunnableCallable接口、ExecutorService框架、synchronized关键字、LockReentrantLock接口、Condition接口、ThreadLocal类以及各种线程池类型。熟练掌握这些工具和技术,是成为高效Java多线程程序员的基础。

多线程的概念

在计算机科学中,多线程是指从软件或者硬件第一级(编程语言层面,操作系统层面,硬件层面)支持执行多个线程的操作。在Java中,多线程是指在一个单一的Java虚拟机(JVM)中,同时运行多个执行路径,即多个线程。每个线程都是操作系统进程中的一个执行单元,具有自己的程序计数器、堆栈和局部变量,但它们共享进程的全局变量和资源。

为什么使用多线程?

  1. 资源利用率:多线程可以提高CPU的利用率,特别是在多核处理器系统中,能够同时并行处理多个任务(这里说的是在多核cpu中同一时间并行执行而不是在同一时间间隔内交替执行),从而提高系统的整体性能。
  2. 响应性:在图形用户界面(GUI)应用程序中,多线程确实可以极大地提高应用程序的响应性。
  3. 模块化:多线程可以增强程序的模块化,使得大型或复杂的程序更容易管理和扩展。在多线程编程中,每个线程通常负责执行一个特定的任务或一组相关任务,这可以看作是将程序分解成多个独立运行的组件或模块。
  4. 并发执行:多线程可以实现并发执行,这对于处理大量数据或执行长时间运行的任务非常有用。

多线程的特征

  • 并发性:多个线程可以同时运行,尽管在单核处理器上可以表现为交替执行。
  • 共享资源:线程之间共享相同的内存空间,这意味着它们可以访问和修改相同的变量,但这也可能导致数据不一致的问题,除非采取适当的同步措施。
  • 上下文切换:操作系统在多个线程之间切换执行,这称为上下文切换,会带来一定的开销。
  • 独立性:每个线程都有自己的执行流,它们可以独立于其他线程执行,但也可以通过线程间通信机制协作。

多线程的挑战

  • 同步问题:当多个线程访问和修改共享资源时,必须采取措施防止竞态条件和死锁。
  • 死锁:当两个或多个线程无限期地等待彼此持有的资源时发生。
  • 资源竞争:多个线程对同一资源的访问可能需要排队,导致性能下降。
  • 调试困难:多线程程序的错误往往难以重现和诊断,因为线程的执行顺序可能在每次运行时都不同。

多线程的实现方式

在Java中,实现多线程主要有四种常见的方式:继承Thread类、实现Runnable接口、使用Executor框架以及使用CallableFuture接口。每种方式都有其适用场景和优缺点。

3.1 继承 Thread

继承Thread类是最直接的实现多线程的方式。你需要创建一个Thread类的子类,并重写run()方法,其中包含线程要执行的代码。当通过子类实例调用start()方法时,run()方法会被系统调用,从而开始线程的执行。

示例代码:
public class MyThread extends Thread {@Overridepublic void run() {System.out.println("Hello from " + this.getName());}public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();}
}

优点

  • 直接使用Thread类的方法,如start(), join(), interrupt()等。
  • 可以访问和修改线程的一些属性,如线程名称、优先级。

缺点

  • Java不允许多重继承,因此,如果需要继承其他类,就不能再继承Thread类。
  • 不能直接使用Thread类的其他成员变量。

3.2 实现 Runnable 接口

实现Runnable接口是更常用的多线程实现方式,因为它避免了Java单继承的限制。你需要创建一个实现了Runnable接口的类,并实现run()方法。之后,创建一个Thread对象,将你的Runnable对象作为参数传入,然后调用start()方法开始线程。

示例代码:
public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Hello from " + Thread.currentThread().getName());}public static void main(String[] args) {Thread myThread = new Thread(new MyRunnable(), "My Runnable Thread");myThread.start();}
}

优点

  • 不影响类的继承链,可以继承其他类。
  • 更加灵活,适合复杂的业务逻辑。

缺点

  • 需要额外的Thread对象来启动线程。

3.3 使用 Executor 框架

Executor框架是Java并发工具包(java.util.concurrent)的一部分,提供了更高级别的抽象来管理线程。ExecutorService接口是Executor框架的核心,它提供了一系列的线程管理方法,如submit(), execute(), shutdown(), isTerminated()等。使用ExecutorService可以创建线程池,有效地复用线程,避免频繁创建和销毁线程的开销。

示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ExecutorExample {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) {Runnable worker = new WorkerThread(i);executor.execute(worker);}executor.shutdown();while (!executor.isTerminated()) {// 等待所有线程完成}}
}class WorkerThread implements Runnable {private int id;public WorkerThread(int id) { this.id = id; }@Overridepublic void run() {System.out.println("Hello from WorkerThread " + id);}
}

优点

  • 更好的资源管理,通过线程池可以控制线程数量,避免过多线程导致的系统资源浪费。
  • 提供了更丰富的线程控制方法,如定时执行、批量提交任务等。

缺点

  • 相对于直接使用ThreadRunnable,实现起来稍微复杂一些。

3.4 使用 CallableFuture

Callable接口类似于Runnable,但是它允许线程执行后返回一个结果,并且可以抛出异常。Future接口用于获取Callable执行的结果,或取消任务的执行。

示例代码:
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;public class CallableExample {public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executor = Executors.newSingleThreadExecutor();Future<Integer> future = executor.submit(new MyCallable());int result = future.get(); // 阻塞直到得到结果System.out.println("Result: " + result);executor.shutdown();}
}class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {return 42; // 假设这是一个计算结果}
}

优点

  • 允许线程执行后返回结果,适合需要返回值的任务。
  • 可以抛出异常,提供更完整的错误处理机制。

缺点

  • 相较于Runnable,实现起来稍微复杂,因为需要处理Future和可能的异常。
  • Future.get()方法会阻塞,直到任务完成,需要注意避免在主线程中调用,以免造成UI冻结或其他性能问题。

线程的生命周期

在Java中,一个线程从创建到结束,会经历一系列的状态变化,这些状态构成了线程的生命周期。理解线程生命周期对于正确管理和控制线程非常重要,尤其在处理线程的启动、终止、同步和调度时。

线程状态

新建状态(New)

线程的生命周期始于新建状态,这时线程对象已经被创建,但是start()方法还没有被调用。在这个状态下,线程还没有开始执行任何代码。

就绪状态(Runnable)

当线程对象的start()方法被调用后,线程进入就绪状态。此时,线程已经准备好运行,但是尚未被调度器选中获取CPU时间片。处于就绪状态的线程由操作系统管理,等待CPU资源以便开始执行。

运行状态(Running)

一旦线程被调度器选中并分配到CPU时间片,线程开始执行其run()方法中的代码,此时线程处于运行状态。在运行状态中,线程可能会因为各种原因而暂停执行,如执行完一个时间片、等待I/O操作、等待其他线程释放锁、响应中断或执行sleep()方法等。

阻塞状态(Blocked / Waiting / Timed Waiting)

在执行过程中,线程可能会进入阻塞状态,这通常发生在以下几种情况下:

  • 等待锁:当线程试图获取一个已被其他线程锁定的资源时,它将被阻塞,直到锁被释放。
  • 等待通知:线程调用Object.wait()方法,等待其他线程的notify()notifyAll()通知。
  • 等待定时事件:线程调用Thread.sleep(long millis)Object.wait(long timeout),在指定的时间段内不会被调度。

终止状态(Terminated)

当线程的run()方法执行完毕,或者线程抛出了未捕获的异常,线程将进入终止状态。一旦线程终止,它将不再参与调度,也不能再次启动。线程对象仍然存在于内存中,直到垃圾回收器将其回收。

线程状态转换

线程状态的转换是由Java虚拟机和操作系统共同管理的。以下是一些常见的状态转换:

  • 新建 → 就绪:当start()方法被调用后,线程从新建状态变为就绪状态。
  • 就绪 → 运行:当线程被调度器选中并分配到CPU资源时,从就绪状态变为运行状态。
  • 运行状态 → 就绪状态:当线程的时间片用尽,或者主动让出CPU(如调用yield()方法),它会从运行状态变回就绪状态,等待下一次调度。
  • 运行 → 阻塞:当线程遇到阻塞条件,如等待锁、I/O操作或执行wait()时,从运行状态变为阻塞状态。
  • 阻塞 → 就绪:当阻塞条件解除,如锁被释放、等待时间到期或收到通知,线程从阻塞状态回到就绪状态。
  • 运行 → 终止:当线程的run()方法执行完毕或抛出未捕获异常,线程从运行状态变为终止状态。

线程调度

线程调度是操作系统的一项核心功能,负责确定哪些线程应该在什么时候运行以及运行多长时间。在Java中,线程调度由Java虚拟机(JVM)和底层操作系统协同完成,主要依据线程的优先级和系统的调度策略。

调度策略

操作系统通常采用以下几种调度策略:

  1. 先来先服务(First-Come, First-Served, FCFS):按照线程到达的先后顺序进行调度。
  2. 时间片轮转(Round Robin, RR):将CPU时间分成相等的时间片,每个就绪状态的线程轮流获得一个时间片。
  3. 优先级调度(Priority Scheduling):根据线程的优先级高低进行调度,优先级高的线程优先执行。在Java中,线程的优先级可以通过Thread类的setPriority()方法设置。
  4. 最短作业优先(Shortest Job First, SJF):优先执行预计执行时间最短的线程。

Java线程调度

在Java中,线程调度遵循优先级调度原则,但实际的调度细节取决于底层操作系统。Java虚拟机并不保证线程的优先级一定会直接影响线程的执行顺序,而是尽力按照优先级来调度线程。此外,线程优先级的范围是1(最低)到10(最高),默认优先级为5。

public class PriorityDemo {public static void main(String[] args) {// 创建低优先级线程Thread lowPriorityThread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("Low priority thread is running.");}});//通过Thread类的setPriority()方法来设置线程的优先级。线程的优先级是一个整数值,范围从Thread.MIN_PRIORITY(常量值为1,代表最低优先级)到Thread.MAX_PRIORITY(常量值为10,代表最高优先级)。默认的优先级是Thread.NORM_PRIORITY(常量值为5)lowPriorityThread.setPriority(Thread.MIN_PRIORITY);// 创建高优先级线程Thread highPriorityThread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("High priority thread is running.");}});highPriorityThread.setPriority(Thread.MAX_PRIORITY);// 启动线程lowPriorityThread.start();highPriorityThread.start();}
}

线程调度的重要概念包括:

  • 抢占式调度:在Java中,线程调度是抢占式的,这意味着高优先级的线程可以打断低优先级线程的执行,一旦高优先级的线程可用,它将立即获得CPU时间片。
  • 时间片:每个线程在运行时会获得一个时间片,时间片结束后,线程会回到就绪状态,等待下一轮调度。
  • 线程让步:线程可以通过调用Thread.yield()方法主动放弃剩余的时间片,将CPU让给同优先级或更高优先级的线程。

影响线程调度的因素

  • 线程优先级:高优先级的线程有更大的机会被调度。
  • 线程状态:只有处于就绪状态的线程才能被调度。
  • 系统负载:系统中的线程数量和CPU核心数量会影响线程调度的效率。
  • 操作系统调度策略:底层操作系统的调度策略会对Java线程的调度产生影响。
  • 线程交互:线程间的同步和通信操作,如等待锁或条件变量,会影响线程的调度时机。

调度的不可预测性

Java线程调度的具体行为在不同操作系统和不同JVM实现中可能会有所不同,因此开发者不能完全依赖于线程优先级来保证线程的执行顺序。在设计多线程应用程序时,应考虑到调度的不确定性和不可预测性,避免过度依赖线程调度来实现同步或定时任务。

线程安全

synchronized关键字是Java中用于实现线程安全的基本同步机制之一。它确保了在多线程环境中,任何时刻只有一个线程可以执行被synchronized关键字保护的代码段。这种机制通过内部的互斥锁(也称为监视器锁或内置锁)来实现,该锁与Java对象关联。

synchronized关键字的使用

synchronized关键字可以应用于两种情况:方法和代码块。

  1. synchronized方法
    当你声明一个方法为synchronized时,该方法成为一个同步方法。在该方法执行期间,任何其他线程都不能调用这个对象上的任何synchronized方法。这意味着对象的锁将被持有直到该方法执行完毕。

    public class Counter {private int count = 0;public synchronized void increment() {count++;}
    }
    
  2. synchronized代码块
    你也可以使用synchronized关键字来同步代码块,这允许更细粒度的控制。你必须指定一个对象作为锁,通常是this对象或一个类的静态字段。

    public class Counter {private int count = 0;private final Object lock = new Object();public void increment() {synchronized (lock) {count++;}}
    }
    

synchronized关键字的特点

  • 排他性:在任意时刻,只有一个线程能够执行被synchronized保护的代码。
  • 有序性:由于synchronized关键字的锁是基于对象的,所以它强制执行了变量读取和写入的有序性,避免了指令重排序带来的问题。
  • 可见性:当一个线程更改了共享变量的值,然后释放了锁,另一个线程在获取该锁时能够看到前一个线程所做的更改。

synchronized的局限性

  • 性能开销:由于synchronized需要维护锁的所有权和等待队列,因此在高并发的情况下可能会成为性能瓶颈。
  • 死锁风险:如果多个synchronized代码块或方法没有正确的加锁顺序,可能会导致死锁。

如何优化synchronized

虽然synchronized关键字是实现线程安全的一种简单方式,但在高并发场景下可能不是最优的选择。以下是一些优化策略:

  • 减少锁的范围:只在必要的时候使用synchronized,尽量减小同步代码块的大小。
  • 使用锁分离:如果可能,将共享资源分割,每个资源有自己的锁,这样可以减少锁的竞争。
  • 使用更高效的锁:如java.util.concurrent包中的ReentrantLock,它提供了比synchronized更灵活的锁定机制,如可重入、公平性和条件变量。

示例代码

public class Counter {private int count = 0;private final Object lock = new Object();public void increment() {synchronized (lock) {count++;}}public int getCount() {synchronized (lock) {return count;}}
}

在上面的例子中,Counter类使用一个私有的锁对象来同步对count字段的访问。这确保了incrementgetCount方法在多线程环境下的线程安全性。

7.2 Lock 接口

Lock接口是java.util.concurrent.locks包的一部分,它提供了一种更高级别的锁定机制,比synchronized关键字更灵活、更强大。Lock接口定义了以下主要方法:

  • void lock(): 获取锁。如果锁已被另一个线程持有,则当前线程将一直等待,直到锁被释放。
  • void unlock(): 释放锁。
  • boolean tryLock(): 尝试获取锁。如果锁不可用,则立即返回false,不会阻塞线程。
  • boolean tryLock(long time, TimeUnit unit): 尝试获取锁。如果锁不可用,则等待一定的时间,如果在等待时间内锁仍未被释放,则返回false
  • Condition newCondition(): 返回一个Condition对象,可以用来实现更复杂的线程间同步。
ReentrantLock 类

ReentrantLockLock接口的一个实现,它提供了一个可重入的互斥锁。ReentrantLock有两个构造函数,分别用于创建公平锁和非公平锁:

  • 公平锁:线程按照请求锁的顺序获取锁,这样可以减少线程的饥饿现象,但是性能通常不如非公平锁。
  • 非公平锁:线程获取锁时没有固定的顺序,可能会导致后请求锁的线程在某些情况下先于前面的线程获取锁,这种情况下,锁的获取可能更偏向于当前正在运行的线程,从而提高性能。

ReentrantLock还提供了以下额外的控制功能:

  • 可中断的锁获取lockInterruptibly()允许线程在等待锁时响应中断。
  • 锁的公平性控制:通过构造函数的布尔参数来决定锁是否为公平锁。
  • 锁的重入次数ReentrantLock允许同一个线程多次获取同一个锁,而不会造成死锁。
Condition 接口

Condition接口也是java.util.concurrent.locks包的一部分,它与Lock接口一起使用,提供了一种比Object类的wait()notify()方法更高级的线程等待和唤醒机制。Condition接口允许线程等待某个条件满足,而不仅仅是在对象监视器上等待。

Condition接口的主要方法包括:

  • void await(): 释放锁并使当前线程等待,直到其他线程调用与此Condition相关的signal()signalAll()方法。
  • void signal(): 唤醒一个等待此Condition的线程。
  • void signalAll(): 唤醒所有等待此Condition的线程。
示例代码

下面是一个使用ReentrantLockCondition的示例,展示如何实现一个简单的生产者-消费者模式:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class Buffer {private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();private final Object[] items;private int putIndex, takeIndex, count;public Buffer(int capacity) {if (capacity <= 0) {throw new IllegalArgumentException("Capacity must be greater than 0.");}items = new Object[capacity];}public void put(Object item) throws InterruptedException {lock.lock();try {// 如果缓冲区满,则等待while ((count == items.length) || (putIndex == takeIndex)) {long nanos = 1000 * 1000 * 1000; // 1秒nanos = notFull.awaitNanos(nanos);}items[putIndex] = item;if (++putIndex == items.length) putIndex = 0;++count;// 唤醒等待的消费者线程notEmpty.signal();} finally {lock.unlock();}}public Object take() throws InterruptedException {lock.lock();try {// 如果缓冲区空,则等待long nanos = 1000 * 1000 * 1000; // 1秒while (count == 0 && nanos > 0) {nanos = notEmpty.awaitNanos(nanos);}if (count != 0) {Object x = items[takeIndex];if (++takeIndex == items.length) takeIndex = 0;--count;// 唤醒等待的生产者线程notFull.signal();return x;} else {return null; // 缓冲区仍然为空,返回null}} finally {lock.unlock();}}
}
详细解释
构造函数

构造函数接收一个整数参数capacity,用于指定缓冲区的大小。如果传递的容量小于或等于0,将抛出IllegalArgumentException。构造函数还初始化了items数组,并设置了putIndextakeIndexcount变量的初始值。

put() 方法

生产者线程调用put()方法将对象放入缓冲区。该方法的主要逻辑如下:

  1. 获取锁:

    • 使用lock.lock()获取锁。
  2. 检查缓冲区是否已满:

    • 使用while (count == items.length)检查缓冲区是否已满。
    • 如果缓冲区已满,线程将调用notFull.awaitNanos(nanos)等待,其中nanos是最大等待时间(以纳秒为单位)。
    • awaitNanos()方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
  3. 放入对象:

    • 如果缓冲区未满,生产者将对象放入items数组中。
    • 更新putIndexcount变量。
  4. 唤醒消费者线程:

    • 调用notEmpty.signal()来唤醒等待的消费者线程。
  5. 释放锁:

    • 使用lock.unlock()释放锁。
take() 方法

消费者线程调用take()方法从缓冲区取出对象。该方法的主要逻辑如下:

  1. 获取锁:

    • 使用lock.lock()获取锁。
  2. 检查缓冲区是否为空:

    • 使用while (count == 0)检查缓冲区是否为空。
    • 如果缓冲区为空,线程将调用notEmpty.awaitNanos(nanos)等待,其中nanos是最大等待时间(以纳秒为单位)。
    • awaitNanos()方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
  3. 取出对象:

    • 如果缓冲区不为空,消费者将对象从items数组中取出。
    • 更新takeIndexcount变量。
  4. 唤醒生产者线程:

    • 调用notFull.signal()来唤醒等待的生产者线程。
  5. 释放锁:

    • 使用lock.unlock()释放锁。
使用awaitNanos()方法的原因

使用awaitNanos()方法而不是普通的await()方法有几个好处:

  1. 避免忙等:

    • awaitNanos()方法允许线程在等待期间释放锁,并在指定的时间后自动恢复,从而避免了忙等的情况。
  2. 节省CPU资源:

    • 通过限制等待时间,可以减少CPU的使用率,避免不必要的循环检查。
  3. 提高系统响应性:

    • 如果条件在等待时间内没有满足,线程将退出等待状态并继续执行,这有助于提高系统的响应性。
总结

Lock接口和ReentrantLock类提供了更高级、更灵活的锁控制机制,适用于需要更细粒度控制的场景。使用Lock接口和ReentrantLock时,需要注意锁的获取和释放必须配对,否则会导致死锁或资源泄露。同时,Condition接口提供了更灵活的线程间同步方式,有助于实现更复杂的同步逻辑。

线程间通信

线程间通信概述

线程间通信是指在一个多线程环境中,不同线程之间共享信息和协调行为的过程。这对于确保程序的正确执行和提高效率至关重要。线程间通信通常涉及以下几种机制:

  1. 共享内存:

    • 多个线程共享相同的内存空间,通过读写共享变量来通信。
    • 必须注意同步访问,以防止竞态条件。
  2. 信号量和条件变量:

    • 信号量用于管理资源的访问权限。
    • 条件变量允许线程等待特定条件的满足。
  3. 消息队列:

    • 一种基于队列的数据结构,线程可以向队列发送消息,其他线程可以从队列中读取消息。
  4. 管道(Pipes):

    • 允许线程或进程之间通过管道进行通信。
  5. 事件对象:

    • 用于信号通知,可以用来同步线程。

Java 中的线程间通信

在Java中,最常用的线程间通信机制包括使用Object类的wait()notify()方法,以及使用java.util.concurrent包中的Lock接口和Condition接口。

使用Objectwait()notify()方法

这是Java中最基本的线程间通信方式之一。wait()方法使当前线程等待,直到另一个线程调用notify()notifyAll()方法。这些方法必须在同步块内调用,通常在synchronized块或方法中。

使用Lock接口和Condition接口

Lock接口提供了更高级的锁定机制,而Condition接口则提供了更灵活的线程等待和唤醒机制。这些接口位于java.util.concurrent.locks包中。

示例代码

下面是一个使用ReentrantLockCondition的示例,展示如何实现一个简单的生产者-消费者模式:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class Buffer {private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();private final Object[] items;private int putIndex, takeIndex, count;public Buffer(int capacity) {if (capacity <= 0) {throw new IllegalArgumentException("Capacity must be greater than 0.");}items = new Object[capacity];}public void put(Object item) throws InterruptedException {lock.lock();try {// 如果缓冲区满,则等待while ((count == items.length) || (putIndex == takeIndex)) {long nanos = 1000 * 1000 * 1000; // 1秒nanos = notFull.awaitNanos(nanos);}items[putIndex] = item;if (++putIndex == items.length) putIndex = 0;++count;// 唤醒等待的消费者线程notEmpty.signal();} finally {lock.unlock();}}public Object take() throws InterruptedException {lock.lock();try {// 如果缓冲区空,则等待long nanos = 1000 * 1000 * 1000; // 1秒while (count == 0 && nanos > 0) {nanos = notEmpty.awaitNanos(nanos);}if (count != 0) {Object x = items[takeIndex];if (++takeIndex == items.length) takeIndex = 0;--count;// 唤醒等待的生产者线程notFull.signal();return x;} else {return null; // 缓冲区仍然为空,返回null}} finally {lock.unlock();}}
}

详细解释

构造函数

构造函数接收一个整数参数capacity,用于指定缓冲区的大小。如果传递的容量小于或等于0,将抛出IllegalArgumentException。构造函数还初始化了items数组,并设置了putIndextakeIndexcount变量的初始值。

put() 方法

生产者线程调用put()方法将对象放入缓冲区。该方法的主要逻辑如下:

  1. 获取锁:

    • 使用lock.lock()获取锁。
  2. 检查缓冲区是否已满:

    • 使用while ((count == items.length) || (putIndex == takeIndex))检查缓冲区是否已满。
    • 如果缓冲区已满,线程将调用notFull.awaitNanos(nanos)等待,其中nanos是最大等待时间(以纳秒为单位)。
    • awaitNanos()方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
  3. 放入对象:

    • 如果缓冲区未满,生产者将对象放入items数组中。
    • 更新putIndexcount变量。
  4. 唤醒消费者线程:

    • 调用notEmpty.signal()来唤醒等待的消费者线程。
  5. 释放锁:

    • 使用lock.unlock()释放锁。
take() 方法

消费者线程调用take()方法从缓冲区取出对象。该方法的主要逻辑如下:

  1. 获取锁:

    • 使用lock.lock()获取锁。
  2. 检查缓冲区是否为空:

    • 使用while (count == 0 && nanos > 0)检查缓冲区是否为空。
    • 如果缓冲区为空,线程将调用notEmpty.awaitNanos(nanos)等待,其中nanos是最大等待时间(以纳秒为单位)。
    • awaitNanos()方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
  3. 取出对象:

    • 如果缓冲区不为空,消费者将对象从items数组中取出。
    • 更新takeIndexcount变量。
  4. 唤醒生产者线程:

    • 调用notFull.signal()来唤醒等待的生产者线程。
  5. 释放锁:

    • 使用lock.unlock()释放锁。

使用awaitNanos()方法的原因

使用awaitNanos()方法而不是普通的await()方法有几个好处:

  1. 避免忙等:

    • awaitNanos()方法允许线程在等待期间释放锁,并在指定的时间后自动恢复,从而避免了忙等的情况。
  2. 节省CPU资源:

    • 通过限制等待时间,可以减少CPU的使用率,避免不必要的循环检查。
  3. 提高系统响应性:

    • 如果条件在等待时间内没有满足,线程将退出等待状态并继续执行,这有助于提高系统的响应性。

总结

线程间通信是多线程编程的关键部分,它确保了线程之间的协作和数据一致性。使用Lock接口和Condition接口可以实现更高级、更灵活的同步机制,帮助开发者更好地管理线程间的交互。

避免死锁

避免死锁是多线程编程中的一个重要方面,尤其是在Java中。死锁是一种特殊情况下的资源竞争问题,其中一个或多个线程永久阻塞,因为每个线程都在等待另一个线程持有的锁。为了帮助你完善关于如何避免死锁的内容,我将提供一些关键点和建议。

死锁的四个必要条件

死锁通常由以下四个必要条件引起:

  1. 互斥条件:至少有一个资源必须处于非共享模式,即一次只能被一个进程使用。
  2. 占有并等待:进程已保持至少一个资源,但又等待新的资源。
  3. 非抢占:资源不能被抢占,只能由拥有进程自愿释放。
  4. 循环等待:存在一种进程-资源的循环链,每个进程已占用的资源被下一个进程所期望。

如何避免死锁

为了避免死锁的发生,可以采取以下策略:

1. 破坏互斥条件
  • 资源共享:尽可能使资源可共享,减少独占资源的需求。
  • 避免使用锁:如果可能的话,重新设计代码以避免使用锁。
2. 破坏占有并等待条件
  • 一次性获取所有资源:确保线程在开始执行之前获取所有必需的锁。
  • 按顺序获取锁:如果多个线程需要获取多个锁,则让它们按照固定的顺序获取锁,这样可以避免形成循环等待。
3. 破坏非抢占条件
  • 超时机制:为锁请求添加超时机制,如果超过一定时间无法获得锁,则释放已经持有的锁并稍后再重试。
  • 使用tryLock:使用ReentrantLocktryLock()方法来尝试获取锁,如果锁不可用,则不会阻塞线程。
4. 破坏循环等待条件
  • 锁顺序:如果一个线程需要多个锁,确保总是按照相同的顺序获取这些锁。
  • 死锁检测:定期检查是否有可能出现死锁的情况,如果检测到潜在的死锁,则释放锁并重试。

线程池

10.1 ExecutorService

线程池是Java多线程编程中的一个重要概念,它可以有效地管理线程的创建和销毁过程,减少系统资源的消耗,并提供了一种更灵活的方式来管理并发任务。下面是关于线程池的一些详细信息,可以帮助你更好地理解和使用线程池。

线程池的基本概念

线程池是在Java中管理线程的一种机制,它预先创建一定数量的线程,这些线程处于等待状态,当有新的任务到来时,线程池会分配一个线程来执行这个任务。当任务完成后,线程并不会被销毁,而是返回到线程池中等待下一个任务。这种方式可以避免频繁创建和销毁线程带来的开销,提高程序的执行效率。

Java中的线程池

Java中线程池的主要接口是ExecutorService,它是java.util.concurrent包的一部分。ExecutorService提供了一些重要的方法来控制线程池的生命周期,如submit()execute()shutdown()awaitTermination()等。

创建线程池

线程池可以通过Executors工厂类来创建,该类提供了几个静态方法来创建不同类型的线程池。以下是几种常见的线程池类型:

  1. Fixed Thread Pool (newFixedThreadPool(nThreads)):

    • 固定大小的线程池,线程数量固定。
    • 如果提交的任务数量超过了线程池的大小,这些任务会被放入一个队列中等待执行。
    • 适用于任务数量未知的情况,尤其是处理大量短期异步任务时。
  2. Cached Thread Pool (newCachedThreadPool()):

    • 可缓存的线程池,线程数量动态调整。
    • 当没有任务时,多余的空闲线程会被销毁。
    • 适用于执行大量的短期异步任务。
  3. Scheduled Thread Pool (newScheduledThreadPool(nThreads)):

    • 定时线程池,用于执行周期性或定时任务。
    • 支持延迟执行任务和周期性执行任务。
  4. Single Thread Executor (newSingleThreadExecutor()):

    • 单一线程池,只包含一个线程。
    • 适用于需要保证任务按顺序执行的场合。
线程池参数
  1. 核心线程数 (corePoolSize):

    • 表示线程池中的最小线程数量。即使没有任务执行,线程池也会维护这些线程。
    • 这些线程通常不会被终止,除非调用了allowCoreThreadTimeOut(true)
  2. 最大线程数 (maximumPoolSize):

    • 表示线程池中可以创建的最大线程数量。
    • 当任务队列满时,线程池会继续创建新线程,直到达到最大线程数。
  3. 空闲线程存活时间 (keepAliveTime):

    • 指定了线程空闲时可以存活的时间长度。
    • 对于超过核心线程数的线程,如果它们空闲了指定的时间长度,就会被终止。
    • 对于核心线程,默认情况下,如果设置了allowCoreThreadTimeOut(true),核心线程也会遵守这个时间限制。
  4. 时间单位 (TimeUnit):

    • 用于指定keepAliveTime的时间单位,例如秒(SECONDS)、毫秒(MILLISECONDS)等。
  5. 工作队列 (workQueue):

    • 用于存放等待执行的任务的队列。
    • 通常使用ArrayBlockingQueue, LinkedBlockingQueueSynchronousQueue等。
    • 当线程池中的线程数达到最大线程数时,新来的任务将会被放入此队列等待执行。
  6. 拒绝策略 (handler):

    • 当任务队列已满并且线程池已经达到最大线程数时,如果还有新的任务提交,那么线程池会采取拒绝策略来处理这些任务。
    • 常见的拒绝策略包括:
      • AbortPolicy: 抛出RejectedExecutionException异常。
      • CallerRunsPolicy: 由调用者所在的线程来执行该任务。
      • DiscardPolicy: 不处理该任务(也就是将其丢弃)。
      • DiscardOldestPolicy: 丢弃队列中最旧的任务,然后重试执行当前任务。
示例代码

1. Fixed Thread Pool

import java.util.concurrent.*;public class FixedThreadPoolExample {public static void main(String[] args) {// 设置线程池参数int corePoolSize = 3; // 核心线程数int maximumPoolSize = 3; // 最大线程数long keepAliveTime = 60L; // 空闲线程存活时间TimeUnit unit = TimeUnit.SECONDS; // 时间单位BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略// 创建线程池ExecutorService fixedThreadPool = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler);for (int i = 0; i < 10; i++) {final int taskId = i;fixedThreadPool.submit(() -> {System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}// 关闭线程池fixedThreadPool.shutdown();while (!fixedThreadPool.isTerminated()) {// 等待所有任务完成}System.out.println("All tasks completed.");}
}

2. Cached Thread Pool

import java.util.concurrent.*;public class CachedThreadPoolExample {public static void main(String[] args) {// 设置线程池参数int corePoolSize = 0; // 核心线程数int maximumPoolSize = Integer.MAX_VALUE; // 最大线程数long keepAliveTime = 60L; // 空闲线程存活时间TimeUnit unit = TimeUnit.SECONDS; // 时间单位BlockingQueue<Runnable> workQueue = new SynchronousQueue<>(); // 工作队列RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略// 创建线程池ExecutorService cachedThreadPool = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler);for (int i = 0; i < 10; i++) {final int taskId = i;cachedThreadPool.submit(() -> {System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}// 关闭线程池cachedThreadPool.shutdown();while (!cachedThreadPool.isTerminated()) {// 等待所有任务完成}System.out.println("All tasks completed.");}
}

3. Scheduled Thread Pool

import java.util.concurrent.*;public class ScheduledThreadPoolExample {public static void main(String[] args) {// 设置线程池参数int corePoolSize = 2; // 核心线程数int maximumPoolSize = 2; // 最大线程数long keepAliveTime = 60L; // 空闲线程存活时间TimeUnit unit = TimeUnit.SECONDS; // 时间单位BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略// 创建线程池ScheduledExecutorService scheduledThreadPool = new ScheduledThreadPoolExecutor(corePoolSize,handler);for (int i = 0; i < 10; i++) {final int taskId = i;scheduledThreadPool.scheduleAtFixedRate(() -> {System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}, 0, 2, TimeUnit.SECONDS);}// 关闭线程池scheduledThreadPool.shutdown();while (!scheduledThreadPool.isTerminated()) {// 等待所有任务完成}System.out.println("All tasks completed.");}
}

4. Single Thread Executor

import java.util.concurrent.*;public class SingleThreadExecutorExample {public static void main(String[] args) {// 设置线程池参数int corePoolSize = 1; // 核心线程数int maximumPoolSize = 1; // 最大线程数long keepAliveTime = 0L; // 空闲线程存活时间TimeUnit unit = TimeUnit.MILLISECONDS; // 时间单位BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略// 创建线程池ExecutorService singleThreadExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler);for (int i = 0; i < 10; i++) {final int taskId = i;singleThreadExecutor.submit(() -> {System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}// 关闭线程池singleThreadExecutor.shutdown();while (!singleThreadExecutor.isTerminated()) {// 等待所有任务完成}System.out.println("All tasks completed.");}
}

在上面的每个示例中,我使用ThreadPoolExecutor构造函数显式地设置了线程池参数,并根据每种线程池的特性和用途配置了合适的参数。例如,在FixedThreadPool示例中,核心线程数与最大线程数相同,而在CachedThreadPool示例中,核心线程数为0,最大线程数为Integer.MAX_VALUE,以适应动态任务需求。对于ScheduledThreadPool,虽然它也有核心线程数和最大线程数的参数,但通常我们使用ScheduledThreadPoolExecutor的构造函数来创建定时线程池,而不是直接使用ThreadPoolExecutor

线程中断

线程中断是Java多线程编程中的一个重要特性,它允许一个线程请求另一个线程停止执行。当一个线程被中断时,它会抛出InterruptedException,这通常发生在阻塞操作中,比如Thread.sleep(), Object.wait(), 或者LockSupport.park()等。

如何中断一个线程

要中断一个线程,可以调用线程对象的interrupt()方法。这会设置线程的中断标志,并且如果线程正在执行一个阻塞操作,它会抛出InterruptedException

检查线程中断状态

每个线程都有一个内部中断标志,可以通过以下方法来检查或清除这个标志:

  • Thread.interrupted(): 返回当前线程的中断状态,并清除中断标志。
  • Thread.isInterrupted(): 返回当前线程或给定线程的中断状态,但不会清除中断标志。

示例代码

下面是一个简单的示例,演示如何中断一个线程:

public class InterruptExample {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.println("Thread interrupted");return;}try {Thread.sleep(1000);System.out.println("Thread is running");} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 重新设置中断标志System.out.println("Thread interrupted during sleep");return;}}});thread.start();Thread.sleep(5000); // 等待5秒后中断线程thread.interrupt();thread.join();System.out.println("Main thread finished.");}
}

在这个例子中,主线程创建了一个新线程并启动它。新线程在无限循环中每秒打印一条消息,并检查是否被中断。如果被中断,它会退出循环并结束线程。主线程等待5秒后中断新线程,并等待新线程结束。

注意事项

  • 中断标志: 中断标志是一个线程级别的标志,当线程被中断时,这个标志被设置。当线程抛出InterruptedException时,中断标志会被清除。因此,在捕获InterruptedException后,通常需要重新设置中断标志,以保持中断状态的一致性。
  • 非阻塞操作: 如果线程在非阻塞操作中被中断,它不会抛出InterruptedException。因此,线程应定期检查它的中断状态。
  • 资源清理: 在线程中处理中断时,不要忘记清理任何打开的资源或进行必要的清理操作。

守护线程

守护线程(Daemon Threads)是Java多线程编程中的一个重要概念。它们通常用于执行后台任务,如垃圾收集、日志记录、心跳检测等,这些任务对于程序的正常运行是辅助性的。当程序中的所有用户线程(非守护线程)都结束时,守护线程会自动结束,不需要显式地关闭它们。

守护线程的特点

  1. 自动结束:当Java程序中没有非守护线程在运行时,所有的守护线程都会自动结束,即使它们仍在执行。
  2. 辅助性:守护线程通常用于执行后台任务,这些任务不是程序的主要业务逻辑,但对程序的运行是有益的。
  3. 生命周期:守护线程的生命周期与其他线程相同,但它们的行为受到程序中其他线程的影响。

创建守护线程

要创建一个守护线程,需要在调用Thread.start()方法之前,通过调用Thread.setDaemon(true)方法将线程标记为守护线程。

示例代码

下面是一个简单的示例,展示了如何创建一个守护线程:

public class DaemonThreadExample {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("Daemon thread running...");try {Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();break;}}});// 设置线程为守护线程daemonThread.setDaemon(true);// 启动守护线程daemonThread.start();// 主线程睡眠一段时间后结束try {Thread.sleep(5000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Main thread finished.");}
}

在这个示例中,守护线程每隔一秒打印一条消息。主线程睡眠5秒后结束。由于守护线程是作为守护线程创建的,所以当主线程结束时,守护线程也会自动结束。

注意事项

  1. 守护线程的启动:守护线程必须在调用start()方法之前设置为守护线程,一旦线程开始运行,就不能改变它的守护状态。
  2. 资源释放:如果守护线程持有资源(如文件句柄、网络连接等),则应在主线程结束前确保这些资源被妥善释放,否则可能导致资源泄露。
  3. 异常处理:守护线程通常不应该抛出未捕获的异常,因为这可能导致程序异常终止。因此,最好在守护线程中捕获异常并妥善处理。

线程组

线程组(Thread Group)是Java多线程编程中的一个概念,它用于组织和管理一组线程。线程组提供了一种将线程分组的方式,使得可以对这些线程进行统一的管理和控制。线程组可以嵌套,也就是说,一个线程组可以包含其他的线程组,形成层次结构。

线程组的作用

  • 组织线程:线程组提供了一种将线程按照功能或逻辑进行分类的方法。
  • 管理线程:可以通过线程组来启动、挂起、恢复或终止线程。
  • 线程安全:线程组提供了一种机制来限制哪些线程可以访问或控制其他线程。

创建线程组

要创建一个线程组,可以使用ThreadGroup类的构造函数。通常,线程组会在创建线程时指定。每个线程默认属于其创建者的线程组,如果没有指定线程组,则属于系统的默认线程组。

示例代码

下面是一个简单的示例,展示了如何创建线程组和向其中添加线程:

public class ThreadGroupExample {public static void main(String[] args) {// 创建线程组ThreadGroup group = new ThreadGroup("MyGroup");// 创建线程并将其加入到线程组中Thread thread = new Thread(group, () -> {System.out.println("Hello from " + Thread.currentThread().getName());}, "ThreadInGroup");// 启动线程thread.start();// 输出线程组的信息System.out.println("Thread Group Name: " + group.getName());System.out.println("Active Count: " + group.activeCount());// 等待线程结束try {thread.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}

在这个示例中,我们首先创建了一个名为"MyGroup"的线程组。接着,创建了一个线程,并将其加入到这个线程组中。然后启动了这个线程,并输出了线程组的名称和活动线程的数量。

线程组的方法

ThreadGroup类提供了多种方法来管理和控制线程组内的线程:

  • void destroy(): 销毁线程组及其所有子线程和子线程组(仅当线程组中没有任何活动线程时才可用)。
  • int activeCount(): 返回线程组中当前活动线程的数量。
  • void enumerate(Thread[] threads): 将线程组中当前活动的线程复制到数组中。
  • void checkAccess(): 检查当前线程是否有权限访问该线程组。
  • void stop(): 请求线程组中的所有线程停止执行(不推荐使用,因为这可能会导致资源泄露)。
  • void suspend(): 暂停线程组中的所有线程。
  • void resume(): 恢复线程组中所有被暂停的线程。

注意事项

  1. 安全性:线程组提供了一种安全机制,只有创建线程组的线程才能访问和控制该线程组中的线程。这有助于保护线程不受未经授权的线程的干扰。
  2. 资源管理:线程组可以帮助管理线程的生命周期,比如通过destroy()方法来销毁整个线程组,这在某些情况下可能是有用的。
  3. 局限性:尽管线程组提供了一定程度的管理能力,但在现代Java并发编程中,线程池和ExecutorService等更高级的工具通常被视为更高效和更灵活的选择。线程组主要用于早期版本的Java,现在更多的是作为一种历史遗留的概念。

线程本地存储

线程本地存储(Thread Local Storage, TLS)是Java多线程编程中的一个重要概念,它提供了一种机制,使得每个线程都可以拥有自己独立的变量副本。ThreadLocal类是Java标准库中用于实现这一特性的工具。

什么是线程本地存储?

在多线程环境中,多个线程可能会共享同一个对象或变量。当这些线程试图同时修改这些共享变量时,就需要考虑同步问题,以避免竞态条件和数据不一致。然而,在某些情况下,我们希望每个线程都能拥有自己的变量副本,而不必担心线程之间的同步问题。这就是线程本地存储的目的。

ThreadLocal类的使用

ThreadLocal类提供了一种简单而有效的机制来实现线程本地存储。使用ThreadLocal类时,每个线程都可以拥有一个与该线程绑定的变量副本。这些副本是相互独立的,一个线程对它的副本所做的修改不会影响到其他线程。

创建ThreadLocal实例

创建ThreadLocal实例非常简单,只需要创建一个ThreadLocal对象即可。你可以选择在构造函数中提供一个初始值,或者在需要的时候再设置值。

示例代码

下面是一个简单的示例,展示了如何使用ThreadLocal

public class ThreadLocalExample {private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) {Thread thread1 = new Thread(() -> {threadLocal.set(100);System.out.println("Thread 1: " + threadLocal.get());});Thread thread2 = new Thread(() -> {threadLocal.set(200);System.out.println("Thread 2: " + threadLocal.get());});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Main thread: " + threadLocal.get());}
}

在这个示例中,我们定义了一个ThreadLocal变量threadLocal,并为其提供了一个初始值0。接着,我们在两个不同的线程中分别设置不同的值,并打印出这些值。注意,每个线程中的输出都是独立的,不受其他线程的影响。

ThreadLocal类的方法

ThreadLocal类提供了以下主要方法:

  • get(): 获取当前线程中变量的副本。
  • set(T value): 设置当前线程中变量的副本。
  • remove(): 移除当前线程中的变量副本。
  • initialValue(): 可选方法,返回当前线程中变量的初始值。

注意事项

  1. 内存泄漏:当不再需要某个ThreadLocal变量时,应该调用remove()方法来移除当前线程中的变量副本。否则,即使线程结束了,ThreadLocal变量的副本仍会被保留,这可能导致内存泄漏。
  2. 初始化:默认情况下,ThreadLocal变量的初始值为null。如果需要设置特定的初始值,可以通过覆盖initialValue()方法来实现。
  3. 性能考虑:虽然ThreadLocal可以简化多线程编程,但频繁地调用get()set()方法可能会对性能产生影响,尤其是当线程频繁创建和销毁时。因此,在可能的情况下,尽量减少ThreadLocal的使用频率。

总结

Java多线程技术是Java平台的核心特性之一,它允许开发人员构建高度并发的应用程序,充分利用现代多核处理器的硬件资源。多线程编程虽然强大但也带来了诸多挑战,如竞态条件、死锁、资源竞争等问题。下面是对Java多线程技术的总结和完善:

Java多线程的核心概念

  • 并发性:多个线程可以同时运行,尽管在单核处理器上可以表现为交替执行。
  • 共享资源:线程之间共享相同的内存空间,这意味着它们可以访问和修改相同的变量,但这也可能导致数据不一致的问题,除非采取适当的同步措施。
  • 上下文切换:操作系统在多个线程之间切换执行,这称为上下文切换,会带来一定的开销。
  • 独立性:每个线程都有自己的执行流,它们可以独立于其他线程执行,但也可以通过线程间通信机制协作。

实现多线程的方式

  • 继承Thread:创建Thread类的子类,并重写run()方法。
  • 实现Runnable接口:创建实现了Runnable接口的类,并实现run()方法。
  • 使用Executor框架:通过ExecutorService接口创建线程池来管理线程。
  • 使用CallableFuture接口:创建实现了Callable接口的类,可以返回结果,并使用Future来获取结果。

线程间通信

  • 共享内存:多个线程共享相同的内存空间,通过读写共享变量来通信。必须注意同步访问,以防止竞态条件。
  • 信号量和条件变量:信号量用于管理资源的访问权限。条件变量允许线程等待特定条件的满足。
  • 消息队列:线程可以向队列发送消息,其他线程可以从队列中读取消息。
  • 管道(Pipes):允许线程或进程之间通过管道进行通信。
  • 事件对象:用于信号通知,可以用来同步线程。

使用Objectwait()notify()方法

这是Java中最基本的线程间通信方式之一。wait()方法使当前线程等待,直到另一个线程调用notify()notifyAll()方法。这些方法必须在同步块内调用,通常在synchronized块或方法中。

使用Lock接口和Condition接口

Lock接口提供了更高级的锁定机制,而Condition接口则提供了更灵活的线程等待和唤醒机制。这些接口位于java.util.concurrent.locks包中。

避免死锁

  • 破坏互斥条件:尽可能使资源可共享,减少独占资源的需求;避免使用锁。
  • 破坏占有并等待条件:确保线程在开始执行之前获取所有必需的锁;按顺序获取锁。
  • 破坏非抢占条件:为锁请求添加超时机制;使用ReentrantLocktryLock()方法。
  • 破坏循环等待条件:如果一个线程需要多个锁,确保总是按照相同的顺序获取这些锁;定期检查是否有可能出现死锁的情况。

线程池

  • 固定线程池:通过Executors.newFixedThreadPool()创建固定大小的线程池。
  • 缓存线程池:通过Executors.newCachedThreadPool()创建可以缓存线程的线程池。
  • 单线程执行器:通过Executors.newSingleThreadExecutor()创建只包含一个线程的线程池。
  • 定时线程池:通过Executors.newScheduledThreadPool()创建可以安排任务的线程池。

线程中断

线程可以被中断,以请求线程提前结束。线程可以通过调用Thread.interrupt()方法被中断。当一个线程被中断时,它会抛出InterruptedException,这通常发生在阻塞操作中。

守护线程

守护线程是那些在后台运行,为其他线程服务的线程,当所有非守护线程结束时,守护线程自动结束。守护线程通常用于执行后台任务,如垃圾收集、日志记录等。

线程组

线程组用于组织和管理一组线程。通过ThreadGroup类可以创建线程组,并将线程加入到线程组中,从而方便地管理和控制线程。

线程本地存储

ThreadLocal类提供了一个线程本地变量,每个线程都有自己的副本。ThreadLocal可以用来存储线程特定的数据,避免了线程之间的数据共享和同步问题。

相关文章:

Java 多线程技术详解

文章目录 Java 多线程技术详解目录引言多线程的概念为什么使用多线程&#xff1f;多线程的特征多线程的挑战 多线程的实现方式3.1 继承 Thread 类示例代码&#xff1a; 3.2 实现 Runnable 接口示例代码&#xff1a; 3.3 使用 Executor 框架示例代码&#xff1a; 3.4 使用 Calla…...

一份简单实用的MATLAB M语言编码风格指南

MATLAB M语言编码风格指南 1. 文件命名2. 函数命名3. 注释4. 变量命名5. 布局、注释和文档6. 代码结构7. 错误处理8. 性能优化9. 格式化输出 MATLAB M文件的编码规范对于确保代码的可读性、可维护性和一致性非常重要。下面是一份MATLAB M语言编码规范的建议&#xff0c;可以作为…...

ubuntu 环境下soc 使用qemu

构建vexpress-a9的linux内核 安装依赖的软件 sudo apt install u-boot-tools sudo apt install gcc-arm-linux-gnueabi sudo apt install g-arm-linux-gnueabi sudo apt install gcc#编译内核 下载 linux-5.10.14 linux-5.10.148.tar.gz 配置 sudo tar -xvf linux-5.10.1…...

Centos安装、迁移gitlab

Centos安装迁移gitlab 一、下载安装二、配置rb修改&#xff0c;起服务。三、访问web&#xff0c;个人偏好设置。四、数据迁移1、查看当前GitLab版本2、备份旧服务器的文件3、将上述备份文件拷贝到新服务器同一目录下&#xff0c;恢复GitLab4、停止新gitlab数据连接服务5、恢复备…...

【Python机器学习】朴素贝叶斯——使用Python进行文本分类

目录 准备文本&#xff1a;从文本中构建词向量 训练算法&#xff1a;从词向量计算概率 测试算法&#xff1a;根据现实情况修改分类器 准备数据&#xff1a;文档词袋模型 要从文本中获取特征&#xff0c;需要先拆分文本。这里的特征是来自文本的词条&#xff0c;一个词条是字…...

【linux】Shell脚本三剑客之grep和egrep命令的详细用法攻略

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…...

Spring条件装配:灵活配置你的应用

文章目录 摘要1. 条件装配概述1.1 什么是条件装配1.2 为什么需要条件装配 2. 使用Conditional注解2.1 Conditional注解简介2.2 编写自定义条件类2.3 应用Conditional注解 3. 内置的条件注解3.1 ConditionalOnClass3.2 ConditionalOnMissingBean3.3 ConditionalOnProperty 4. 使…...

【前端 08】简单学习js字符串

JavaScript中的String对象详解 在JavaScript中&#xff0c;字符串&#xff08;String&#xff09;是一种非常基础且常用的数据类型&#xff0c;用于表示文本数据。虽然JavaScript中的字符串是原始数据类型&#xff0c;但它们的行为类似于对象&#xff0c;因为JavaScript为字符…...

【LLM】-07-提示工程-聊天机器人

目录 1、给定身份 1.1、基础代码 1.2、聊天机器人 2、构建上下文 3、订餐机器人 3.1、窗口可视化 3.2、构建机器人 3.3、创建JSON摘要 利用会话形式&#xff0c;与具有个性化特性&#xff08;或专门为特定任务或行为设计&#xff09;的聊天机器人进行深度对话。 在 Ch…...

AvaloniaUI的学习

相关网站 github:https://github.com/AvaloniaUI/Avalonia 官方中文文档&#xff1a;https://docs.avaloniaui.net/zh-Hans/docs/welcome IDE选择 VS2022VSCodeRider 以上三种我都尝试过&#xff0c;体验Rider最好。VS2022的提示功能不好&#xff0c;VSCode太慢&#xff0c…...

刷题——快速排序

【全网最清晰快速排序&#xff0c;看完快排思想和代码全部通透&#xff0c;不通透你打我&#xff01;-哔哩哔哩】 https://b23.tv/8GxEKIk 代码详解如上 #include <iostream> using namespace std;int getPort(int* a, int low, int high) {int port a[low];while(low…...

VPN,实时数据显示,多线程,pip,venv

VPN和翻墙在本质上是不同的。想要真正实现翻墙&#xff0c;需要选择部署在墙外的VPN服务。VPN也能隐藏用户的真实IP地址 要实现Python对网页数据的定时实时采集和输出&#xff0c;可以使用Python的定时任务调度模块。其中一个常用的库是APScheduler。您可以编写一个函数&#…...

自然语言处理(NLP)

自然语言处理&#xff08;NLP&#xff09;是计算机科学与人工智能领域的一个重要研究方向&#xff0c;它致力于让计算机能够理解、分析、处理和生成人类语言。在NLP领域&#xff0c;存在着许多常见的任务&#xff0c;这些任务通常对应着不同的算法和技术。以下将详细列举几个NL…...

Spring Boot集成Spire.doc实现对word的操作

1.什么是spire.doc? Spire.Doc for Java 是一款专业的 Java Word 组件&#xff0c;开发人员使用它可以轻松地将 Word 文档创建、读取、编辑、转换和打印等功能集成到自己的 Java 应用程序中。作为一款完全独立的组件&#xff0c;Spire.Doc for Java 的运行环境无需安装 Micro…...

在Spring Boot中优化if-else语句

在Spring Boot中&#xff0c;优化if-else语句是提升代码质量、增强可读性和可维护性的重要手段。过多的if-else语句不仅会使代码变得复杂难懂&#xff0c;还可能导致代码难以扩展和维护。以下将介绍七种在Spring Boot中优化if-else语句的实战方法&#xff0c;每种方法都将结合示…...

【Django】开源前端库bootstrap,常用

文章目录 下载bootstrap源文件到本地项目引入bootstrap文件 官网&#xff1a;https://www.bootcss.com/V4版本入口&#xff1a;https://v4.bootcss.com/V5版本入口&#xff1a;https://v5.bootcss.com/ 这里使用成熟的V4版本&#xff0c;中文文档地址&#xff1a;https://v4.b…...

2024后端开发面试题总结

一、前言 上一篇离职贴发布之后仿佛登上了热门&#xff0c;就连曾经阿里的师兄都看到了我的分享&#xff0c;这波流量真是受宠若惊&#xff01; 回到正题&#xff0c;文章火之后&#xff0c;一些同学急切想要让我分享一下面试内容&#xff0c;回忆了几个晚上顺便总结一下&#…...

opencascade AIS_Manipulator源码学习

前言 AIS_Manipulator 是 OpenCASCADE 库中的一个类&#xff0c;用于在3D空间中对其他交互对象或一组对象进行局部变换。该类提供了直观的操控方式&#xff0c;使用户可以通过鼠标进行平移、缩放和旋转等操作。 详细功能 交互对象类&#xff0c;通过鼠标操控另一个交互对象…...

Hadoop、Hive、HBase、数据集成、Scala阶段测试

姓名&#xff1a; 总分&#xff1a;Hadoop、Hive、HBase、数据集成、Scala阶段测试 一、选择题&#xff08;共20道&#xff0c;每道0.5分&#xff09; 1、下面哪个程序负责HDFS数据存储&#xff08; C &#xff09; A. NameNode B. Jobtracher C. DataNode D. Sec…...

go语言day19 使用git上传包文件到github Gin框架入门

git分布式版本控制系统_git切换head指针-CSDN博客 获取请求参数并和struct结构体绑定_哔哩哔哩_bilibili &#xff08;gin框架&#xff09; GO: 引入GIn框架_go 引入 gin-CSDN博客 使用git上传包文件 1&#xff09;创建一个github账户&#xff0c;进入Repositories个人仓…...

Ubuntu升级软件或系统

Ubuntu升级软件或系统 升级Ubuntu系统通常是一个相对简单的过程&#xff0c;但在进行操作之前&#xff0c;请务必备份重要数据以防万一。下面是升级Ubuntu系统的一般步骤&#xff1a; 使用软件更新工具升级系统 打开终端&#xff1a; 按下 Ctrl Alt T 组合键打开终端。 更…...

【Redis】Centos7 安装 redis(详细教程)

查看当前 Redis 版本&#xff1a; 当前的 redis 版本太老了&#xff0c;选择安装 Redis5。 一、使用 yum 安装 1、首先安装 scl 源 yum install centos-release-scl-rh 由于我之前已经安装过了&#xff0c;所以加载速度比较快&#xff0c;且显示已经安装成功&#xff0c;是最…...

Hakuin:一款自动化SQL盲注(BSQLI)安全检测工具

关于Hakuin Hakuin是一款功能强大的SQL盲注漏洞安全检测工具&#xff0c;该工具专门针对BSQLi设计&#xff0c;可以帮助广大研究人员优化BSQLi测试用例&#xff0c;并以自动化的形式完成针对目标Web应用程序的漏洞扫描与检测任务。 该工具允许用户以轻松高效的形式对目标Web应…...

在 Postman 中设置全局 token

目录 问题描述解决方案 问题描述 在使用 Postman 进行接口测试时&#xff0c;经常会遇到在 Header 中添加 token 的情况。当接口数量较多时&#xff0c;需要为每个接口进行设置&#xff0c;而且当 token 失效时需要重新获取并设置&#xff0c;这样一来效率较低。 解决方案 下…...

Linux C编程:打造一个插件系统

title: ‘Linux C编程:打造一个插件系统’ date: 2017-03-07 21:16:36 tags: linux C layout: post comments: true 运行环境&#xff1a;linux 使用语言&#xff1a;c 或者c 插件&#xff0c;很多人用过&#xff0c;比如游戏插件&#xff0c;编辑器插件这些&#xff0c; 最著…...

基于毫米波生物感知雷达+STM32设计的独居老人居家监护系统(微信小程序)(192)

基于毫米波生物感知雷达设计的独居老人居家监护系统(微信小程序)(192) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】60G毫米波生物感知雷达原理【3】ESP8266模块配置【4】供电方式1.3 项目开发背景【1】选题的意义…...

C++——类和对象(下)

目录 一、再探构造函数 1.基本定义以及用法 2.必须在初始化列表初始化的成员变量 3.成员变量声明位置的缺省值&#xff08;C11&#xff09; 4.成员变量初始化顺序 二、隐式类型转换 三、static成员 四、友元 五、内部类 六、匿名对象 七、日期类实现 一、再探构造函数…...

Android中集成前端页面探索(Capacitor 或 Cordova 插件)待完善......

探索目标&#xff1a;Android中集成前端页面 之前使用的webview加载html页面&#xff0c;使用bridge的方式进行原生安卓和html页面的通信的方式&#xff0c;探索capacitor-android插件是如何操作的 capacitor-android用途 Capacitor 是一个用于构建现代跨平台应用程序的开源框…...

玩转CSS:用ul li +JS 模拟select,避坑浏览器不兼容。

玩转CSS&#xff1a;用ul li JS 模拟select&#xff0c;避坑浏览器不兼容。 在前端的工作中&#xff0c;经常会遇到 selcet控件&#xff0c;但我们用css来写它的样式时候&#xff0c;总是不那么令人满意&#xff0c;各种浏览器不兼容啊有没有&#xff1f; 那么&#xff0c;我…...

介绍下PolarDB

业务中用的是阿里云自研的PolarDB&#xff0c;分析下PolarDB的架构。 认识PolarDB 介绍 PolarDB是阿里云自研的&#xff0c;兼容MySQL、PostageSQL以及支持MPP的PolarDB-X的高可用、高扩展性的数据库。 架构 部署 云起实验室 - 阿里云开发者社区 - 阿里云 (aliyun.com) 数…...

基于微信小程序+SpringBoot+Vue的儿童预防接种预约系统(带1w+文档)

基于微信小程序SpringBootVue的儿童预防接种预约系统(带1w文档) 基于微信小程序SpringBootVue的儿童预防接种预约系统(带1w文档) 开发合适的儿童预防接种预约微信小程序&#xff0c;可以方便管理人员对儿童预防接种预约微信小程序的管理&#xff0c;提高信息管理工作效率及查询…...

go语言day15 goroutine

Golang-100-Days/Day16-20(Go语言基础进阶)/day17_Go语言并发Goroutine.md at master rubyhan1314/Golang-100-Days GitHub 第2讲-调度器的由来和分析_哔哩哔哩_bilibili 一个进程最多可以创建多少个线程&#xff1f;-CSDN博客 引入协程 go语言中内置了协程goroutine&#…...

Mindspore框架循环神经网络RNN模型实现情感分类|(六)模型加载和推理(情感分类模型资源下载)

Mindspore框架循环神经网络RNN模型实现情感分类 Mindspore框架循环神经网络RNN模型实现情感分类|&#xff08;一&#xff09;IMDB影评数据集准备 Mindspore框架循环神经网络RNN模型实现情感分类|&#xff08;二&#xff09;预训练词向量 Mindspore框架循环神经网络RNN模型实现…...

System类

System类常见方法 ① exit 退出当前程序 public static void main(String[] args) {System.out.println("ok1");//0表示状态&#xff0c;即正常退出System.exit(0);System.out.println("ok2");} ② arraycopy 复制数组元素 复制的数组元素个数必须<原数…...

【前端 02】新浪新闻项目-初步使用CSS来排版

在今天的博文中&#xff0c;我们将围绕“新浪新闻”项目&#xff0c;深入探讨HTML和CSS在网页制作中的基础应用。通过具体实例&#xff0c;我们将学习如何设置图片、标题、超链接以及文本排版&#xff0c;同时了解CSS的引入方式和选择器优先级&#xff0c;以及视频和音频标签的…...

HarmonyOS和OpenHarmony区别联系

前言 相信我们在刚开始接触鸿蒙开发的时候经常看到HarmonyOS和OpenHarmony频繁的出现在文章和文档之中&#xff0c;那么这两个名词分别是什么意思&#xff0c;他们之间又有什么联系呢&#xff1f;本文将通过现有的文章和网站内容并与Google的AOSP和Android做对比&#xff0c;带…...

llama模型,nano

目录 llama模型 Llama模型性能评测 nano模型是什么 Gemini Nano模型 参数量 MMLU、GPQA、HumanEval 1. MMLU(Massive Multi-task Language Understanding) 2. GPQA(Grade School Physics Question Answering) 3. HumanEval llama模型 Large Language Model AI Ll…...

ElasticSearch的应用场景和优势

ElasticSearch是一个开源的分布式搜索和分析引擎&#xff0c;它以其高性能、可扩展性和实时性在多个领域得到了广泛应用。以下是ElasticSearch的主要应用场景和优势&#xff1a; 应用场景 实时搜索&#xff1a; ElasticSearch以其快速、可扩展和实时的特性&#xff0c;成为实…...

git 、shell脚本

git 文件版本控制 安装git yum -y install git 创建仓库 将文件提交到暂存 git add . #将暂存区域的文件提交仓库 git commit -m "说明" #推送到远程仓库 git push #获取远程仓库的更新 git pull #克隆远程仓库 git clone #分支&#xff0c;提高代码的灵活性 #检查分…...

阿里云服务器 篇六:GitHub镜像网站

文章目录 系列文章搭建镜像网站的2种方式使用 Web 抓取工具 (Spider 技术)使用 Web 代理服务器使用 nginx 搭建GitHub镜像网站基础环境搭建添加对 github.com 的转发配置添加对 raw.githubusercontent.com 的转发配置配置更改注意事项(可选)缓存优化为新增设的二级域名配置DN…...

强化学习学习(三)收敛性证明与DDPG

文章目录 证明收敛&#xff1f; Deep RL with Q-FunctionsDouble Q-Learning理论上的解法实际上的解法 DDPG: Q-Learning with continuous actionsAdvanced tips for Q-Learning 证明收敛&#xff1f; 对于Value迭代&#xff1a;不动点证明的思路 首先定义一个算子 B : B V ma…...

培养前端工程化思维,不要让一行代码毁了整个程序

看《阿丽亚娜 5 号&#xff08;Ariane 5&#xff09;火箭爆炸》有感。 1、动手写项目之前&#xff0c;先进行全局性代码逻辑思考&#xff0c;将该做的事情&#xff0c;一些细节&#xff0c;统一建立标准&#xff0c;避免为以后埋雷。 2、避免使用不必要或无意义的代码、注释。…...

电子文件怎么盖章?

电子文件怎么盖章&#xff1f;电子文件盖章是数字化办公中常见的操作&#xff0c;包括盖电子公章和电子骑缝章。以下是针对这两种情况的详细步骤&#xff1a; 一、盖电子公章 方法一&#xff1a;使用专业软件 选择软件&#xff1a;选择一款专业的电子签名或PDF编辑软件&…...

IDEA在编译的时候报Error: java: 找不到符号符号: 变量 log lombok失效问题

错误描述 idea因为lombok的报错: java: You arent using a compiler supported by lombok, so lombok will not work and has been disabled.Your processor is: com.sun.proxy.$Proxy8Lombok supports: sun/apple javac 1.6, ECJ 原因&#xff1a;这是由于Lombok的版本过低的…...

【Python】如何修改元组的值?

一、题目 We have seen that lists are mutable (they can be changed), and tuples are immutable (they cannot be changed). Lets try to understand this with an example. You are given an immutable string, and you want to make chaneges to it. Example >>…...

【安卓】Android Studio简易计算器(实现加减乘除,整数小数运算,正数负数运算)

目录 前言 运算效果 一、创建一个新的项目 二、编写xml文件&#xff08;计算器显示页面&#xff09; 三、实现Java运算逻辑 ​编辑 完整代码 xml文件代码&#xff1a; Java文件代码&#xff1a; 注&#xff1a; 前言 随着移动互联网的普及&#xff0c;手机应用程序已…...

一个vue mixin 小案例,实现等比例缩放

mixin.js /** Author: jinjianwei* Date: 2024-07-24 16:17:16* Description: 等比例缩放&#xff0c;屏幕适配 mixin 函数*/// * 默认缩放值 const scale {width: 1,height: 1, } // * 设计稿尺寸&#xff08;px&#xff09; const baseWidth 1920 const baseHeight 1080 …...

【数据结构初阶】单链表经典算法题十二道——得道飞升(中篇)

hi&#xff0c;bro—— 目录 5、 链表分割 6、 链表的回文结构 7、 相交链表 8、 环形链表 【思考】 —————————————— DEAD POOL —————————————— 5、 链表分割 /* struct ListNode {int val;struct ListNode *next;ListNode(int x) : val(x), …...

CTF ssrf 基础入门 (一)

0x01 引言 我发现我其实并不是很明白这个东西&#xff0c;有些微妙&#xff0c;而且记忆中也就记得Gopherus这个工具了&#xff0c;所以重新学习了一下&#xff0c;顺便记录一下吧 0x02 辨别 我们拿到一个题目&#xff0c;他的名字可能就是题目类型&#xff0c;但是也有可能…...

IP地址在后端怎么存才好?

目录 一、地址的区别 二、字符串存取 2.1 IPV4空间大小 2.2 IPV6空间大小 三、整数存取 四、总结 4.1 字符串存取优缺点 4.2 整数存取的优缺点 一、地址的区别 在网络中&#xff0c;IP地址分为IPV4和IPV6&#xff0c;IPV4是一共占32位的&#xff0c;每8位小数点分隔&…...