JUC面试知识点手册

第一章:Java并发简介

1.1 什么是并发编程

并发编程是指在同一时间段内执行多个任务的编程方式。在单核处理器上,并发通过时间分片来实现,即在同一时间只有一个任务在执行,其他任务被暂停等待。在多核处理器上,并发可以通过同时执行多个任务来实现。

并发编程的主要目的是提高程序的执行效率,特别是在处理I/O操作、网络请求或大规模计算时,并发可以显著减少程序的响应时间。

并发编程的挑战

  • 线程安全:多个线程访问共享资源时,可能会导致数据不一致问题。
  • 死锁:多个线程互相等待对方释放资源,导致程序永远无法继续执行。
  • 线程饥饿:某些线程长时间无法获得执行机会。
  • 上下文切换:线程切换带来的开销,可能导致性能下降。
1.2 Java中的并发编程模型

Java提供了多种并发编程模型,以帮助开发者实现并发处理。主要的模型包括:

  1. 线程(Thread)

    • Java中的每个线程都是操作系统级别的线程,由java.lang.Thread类来表示。开发者可以通过继承Thread类或实现Runnable接口来创建线程。

    • 示例

      public class MyThread extends Thread {
          @Override
          public void run() {
              System.out.println("Thread is running");
          }
      
          public static void main(String[] args) {
              MyThread thread = new MyThread();
              thread.start();
          }
      }
      
  2. 线程池(Thread Pool)

    • 线程池是一种管理线程的机制,它避免了频繁创建和销毁线程的开销,提高了程序的执行效率。Java通过Executor框架提供了线程池的支持。

    • 示例

      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      
      public class ThreadPoolExample {
          public static void main(String[] args) {
              ExecutorService executor = Executors.newFixedThreadPool(5);
      
              for (int i = 0; i < 10; i++) {
                  executor.submit(() -> {
                      System.out.println("Thread is running");
                  });
              }
      
              executor.shutdown();
          }
      }
      
  3. 并发工具包(java.util.concurrent)

    • Java提供了java.util.concurrent包,内含大量并发工具类,如锁机制(ReentrantLock)、同步工具(CountDownLatchCyclicBarrier)、并发集合(ConcurrentHashMap)等,这些工具类简化了并发编程中的复杂性。
1.3 线程的生命周期

Java中的线程生命周期可以分为以下几个状态:

  1. 新建(New):线程对象创建后,还未调用start()方法。
  2. 就绪(Runnable):调用start()方法后,线程进入就绪状态,等待CPU分配执行时间。
  3. 运行(Running):线程获得CPU时间片后,开始执行run()方法中的代码。
  4. 阻塞(Blocked):线程等待某些资源(如锁)时,会进入阻塞状态,等待资源变为可用。
  5. 等待(Waiting):线程通过调用wait()方法进入等待状态,等待其他线程的通知(notify()notifyAll())。
  6. 超时等待(Timed Waiting):线程通过调用sleep()wait(long timeout)等方法进入超时等待状态,指定时间后自动返回就绪状态。
  7. 终止(Terminated):线程的run()方法执行完毕,或者因异常退出,线程进入终止状态,无法再执行。
第一章小结

本章主要介绍了并发编程的基本概念以及Java中提供的并发编程模型。理解这些基础概念对于后续深入学习JUC相关知识点非常重要。下一章将深入探讨Java中用于实现同步的基础工具。
在这里插入图片描述

第二章:基础同步工具

在并发编程中,确保多个线程在访问共享资源时不会引起数据不一致问题是至关重要的。Java提供了一些基础同步工具来帮助开发者管理线程间的同步。在本章中,我们将详细介绍SynchronizedVolatile这两个最基本的同步工具。

2.1 synchronized关键字
2.1.1 原理

synchronized是Java中用于实现线程同步的关键字,它可以用来修饰方法或代码块,以确保同一时间只有一个线程能够执行被修饰的代码。这是通过对象的监视器(Monitor)锁来实现的。

每个对象都有一个隐含的监视器锁,当一个线程试图执行一个被synchronized修饰的方法或代码块时,它首先必须获得该对象的监视器锁。一旦获得锁,其他线程将无法访问该对象的任何其他同步方法或代码块,直到锁被释放。

2.1.2 应用场景

synchronized关键字主要用于以下场景:

  • 线程安全的类:如果某个类的多个线程实例可能会同时访问共享资源,就需要使用synchronized来保证线程安全。
  • 临界区保护:在某些需要确保数据一致性的代码段,可以通过synchronized来实现对临界区的保护。
2.1.3 代码实现与示例
示例1:方法级别的synchronized
public class SynchronizedExample {

    private int counter = 0;

    // 使用synchronized修饰方法,确保线程安全
    public synchronized void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        // 启动两个线程进行递增操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

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

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

        // 输出结果
        System.out.println("Counter: " + example.getCounter());
    }
}

在这个示例中,increment()方法被synchronized修饰,确保了在多线程环境下counter变量的递增操作是线程安全的。

示例2:代码块级别的synchronized
public class SynchronizedBlockExample {

    private final Object lock = new Object();
    private int counter = 0;

    public void increment() {
        synchronized (lock) {
            counter++;
        }
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        SynchronizedBlockExample example = new SynchronizedBlockExample();

        // 启动两个线程进行递增操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

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

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

        // 输出结果
        System.out.println("Counter: " + example.getCounter());
    }
}

在这个示例中,synchronized被应用于代码块级别,通过锁对象lock来实现同步。这种方式的好处是可以灵活控制锁的粒度,避免不必要的锁竞争。

2.2 volatile关键字
2.2.1 原理

volatile关键字是用于声明变量的一个修饰符,用来确保变量在多个线程之间的可见性。当一个变量被声明为volatile时,所有线程在读取该变量时会直接从主内存中读取,而不是从线程的工作内存中缓存。

此外,volatile还会禁止指令重排序,确保了变量的赋值操作在程序中按预期顺序执行。

2.2.2 应用场景

volatile适用于以下场景:

  • 状态标记:当某个标记变量需要在多个线程之间共享时,可以使用volatile来确保其可见性。
  • 轻量级同步:在某些情况下,使用volatile可以避免使用锁,从而提高性能。
2.2.3 代码实现与示例
示例1:volatile状态标记
public class VolatileExample {

    private static volatile boolean flag = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag) {
                // 等待flag变为false
            }
            System.out.println("Thread 1 has exited.");
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false; // 修改flag的值
            System.out.println("Flag has been set to false.");
        });

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

在这个示例中,flag变量被声明为volatile,确保了线程t1能够及时看到线程t2flag变量的修改,避免了无限循环的发生。

示例2:防止指令重排序
public class ReorderingExample {

    private int x = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 42;              // 1
        flag = true;         // 2
    }

    public void reader() {
        if (flag) {          // 3
            System.out.println(x);  // 4
        }
    }

    public static void main(String[] args) {
        ReorderingExample example = new ReorderingExample();

        Thread t1 = new Thread(example::writer);
        Thread t2 = new Thread(example::reader);

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

在这个示例中,flag被声明为volatile,保证了线程t2在判断flagtrue时,能够看到线程t1x的赋值操作,避免了由于指令重排序引发的不可预测行为。

第二章小结

本章介绍了Java中用于实现同步的两种基础工具:synchronizedvolatilesynchronized通过对象的监视器锁实现了对临界区的保护,确保了线程的安全性。而volatile则通过保证变量的可见性和防止指令重排序,为多线程环境提供了轻量级的同步机制。

下一章将深入探讨Java中的锁机制,包括ReentrantLockReadWriteLockStampedLock等高级锁的应用与实现。
在这里插入图片描述

第三章:锁机制

在并发编程中,锁机制是确保多线程访问共享资源时避免数据不一致和竞争条件的重要工具。Java中的JUC(Java并发工具包)提供了多种锁实现,以应对不同的并发场景。在本章中,我们将详细介绍ReentrantLockReadWriteLockStampedLock的原理、应用场景及代码实现。

3.1 ReentrantLock
3.1.1 原理

ReentrantLock 是一个可重入的、独占锁。所谓可重入,意味着同一线程可以多次获取同一个锁而不会发生死锁。ReentrantLock 提供了与synchronized类似的功能,但它更加灵活,可以响应中断请求、支持公平锁和非公平锁、以及提供了尝试获取锁的功能。

  • 公平锁:线程按照请求锁的顺序获取锁。
  • 非公平锁:线程可以“插队”,抢占锁的机会更大,可能导致某些线程长期得不到锁。
3.1.2 应用场景

ReentrantLock 适用于需要更精细控制锁的场景,例如:

  • 需要响应中断的锁操作
  • 需要超时获取锁
  • 需要公平锁机制
  • 复杂的同步控制结构
3.1.3 代码实现与示例
示例1:使用ReentrantLock实现线程安全
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {

    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            counter++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        // 启动两个线程进行递增操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

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

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

        // 输出结果
        System.out.println("Counter: " + example.getCounter());
    }
}

在这个示例中,我们使用ReentrantLock来保护对counter变量的操作,确保它在线程间的操作是安全的。

示例2:使用ReentrantLock响应中断
import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockExample {

    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        try {
            lock.lockInterruptibly(); // 可以响应中断的锁获取
            try {
                // 业务逻辑代码
                System.out.println(Thread.currentThread().getName() + " is performing a task.");
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " was interrupted.");
        }
    }

    public static void main(String[] args) {
        InterruptibleLockExample example = new InterruptibleLockExample();

        Thread t1 = new Thread(example::performTask);
        Thread t2 = new Thread(example::performTask);

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

        // 中断其中一个线程
        t2.interrupt();
    }
}

在这个示例中,ReentrantLocklockInterruptibly()方法允许线程在等待锁的过程中响应中断。这种机制在需要快速响应中断信号的应用场景中非常有用。

3.2 ReadWriteLock
3.2.1 原理

ReadWriteLock 是一种分离读锁和写锁的锁机制。它包含两个锁:一个读锁(共享锁)和一个写锁(独占锁)。多个线程可以同时获取读锁,但只有一个线程可以获取写锁,且在写锁被持有期间,其他线程不能获取读锁。

  • 读锁:允许多个线程同时读取,不会造成冲突。
  • 写锁:只允许一个线程写入,其他线程不能读取或写入。
3.2.2 应用场景

ReadWriteLock 非常适用于读多写少的场景,例如:

  • 配置数据的读取和更新
  • 缓存的访问和刷新
  • 大量读操作、少量写操作的场景
3.2.3 代码实现与示例
示例1:使用ReadWriteLock实现高效读写
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int value = 0;

    public void readValue() {
        lock.readLock().lock(); // 获取读锁
        try {
            System.out.println(Thread.currentThread().getName() + " read value: " + value);
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public void writeValue(int newValue) {
        lock.writeLock().lock(); // 获取写锁
        try {
            value = newValue;
            System.out.println(Thread.currentThread().getName() + " wrote value: " + value);
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        Runnable readTask = example::readValue;
        Runnable writeTask = () -> example.writeValue((int) (Math.random() * 100));

        // 启动多个读线程
        for (int i = 0; i < 5; i++) {
            new Thread(readTask).start();
        }

        // 启动一个写线程
        new Thread(writeTask).start();
    }
}

在这个示例中,多个线程可以同时获取读锁并读取值,而写锁则确保了只有一个线程可以同时写入值,避免了并发写入时的冲突。

3.3 StampedLock
3.3.1 原理

StampedLock 是一种改进的读写锁,除了传统的读锁和写锁外,还提供了一种乐观读锁。乐观读锁允许在没有实际加锁的情况下读取数据,在数据没有发生变化时,可以避免加锁的开销,从而提高性能。

  • 乐观读锁:初始状态下不加锁,允许读操作。如果在读的过程中没有其他线程写入,则读取成功,否则需要升级为悲观读锁或重新读取。
  • 悲观读锁:与ReadWriteLock中的读锁类似。
  • 写锁:与ReadWriteLock中的写锁类似。
3.3.2 应用场景

StampedLock 适用于高并发的读操作,尤其是在大多数情况下读操作不会受到写操作影响的场景。例如:

  • 数据的监控与分析系统
  • 大数据处理中的并行计算
3.3.3 代码实现与示例
示例1:使用StampedLock的乐观读锁
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {

    private final StampedLock lock = new StampedLock();
    private int value = 0;

    public int readValue() {
        long stamp = lock.tryOptimisticRead(); // 尝试获取乐观读锁
        int currentValue = value; // 读取共享变量

        if (!lock.validate(stamp)) { // 检查在读取过程中是否有写操作
            stamp = lock.readLock(); // 获取悲观读锁
            try {
                currentValue = value;
            } finally {
                lock.unlockRead(stamp); // 释放悲观读锁
            }
        }

        return currentValue;
    }

    public void writeValue(int newValue) {
        long stamp = lock.writeLock(); // 获取写锁
        try {
            value = newValue;
        } finally {
            lock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void main(String[] args) {
        StampedLockExample example = new StampedLockExample();

        Runnable readTask = () -> {
            System.out.println(Thread.currentThread().getName() + " read value: " + example.readValue());
        };
        Runnable writeTask = () -> example.writeValue((int) (Math.random() * 100));

        // 启动多个读线程
        for (int i = 0; i < 5; i++) {
            new Thread(readTask).start();
        }

        // 启动一个

写线程
        new Thread(writeTask).start();
    }
}

在这个示例中,StampedLock的乐观读锁允许多个线程以最小的开销读取数据,并且在数据没有被其他线程修改的情况下,可以避免升级为悲观读锁,提高了系统的整体性能。

第三章小结

本章详细介绍了Java中的几种主要锁机制,包括ReentrantLockReadWriteLockStampedLock。这些锁为不同的并发场景提供了灵活而高效的解决方案,帮助开发者在复杂的并发环境中保持数据的一致性和系统的高性能。

下一章将介绍Java中的同步辅助工具,如CountDownLatchCyclicBarrierSemaphore等,它们在多线程协作中扮演了重要角色。
在这里插入图片描述

第四章:同步辅助工具

在并发编程中,线程之间的协作是非常重要的一部分。Java提供了多种同步辅助工具,用于管理线程的协作与协调。本章将详细介绍CountDownLatchCyclicBarrierSemaphoreExchanger的原理、应用场景及代码实现。

4.1 CountDownLatch
4.1.1 原理

CountDownLatch 是一种同步辅助工具,用于等待一组线程完成操作后再继续执行。它通过一个计数器来实现,该计数器初始值为线程的数量,每当一个线程完成其操作时,计数器减1。当计数器达到零时,所有等待的线程才会继续执行。

4.1.2 应用场景

CountDownLatch 适用于需要等待多个线程执行完成后再继续执行的场景。例如:

  • 主线程等待多个工作线程完成初始化
  • 测试用例中等待多个服务启动后执行测试
  • 并行计算中等待各个子任务完成后再汇总结果
4.1.3 代码实现与示例
示例1:主线程等待多个线程完成
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working.");
                try {
                    Thread.sleep(1000); // 模拟工作任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 减少计数
                }
            }).start();
        }

        // 主线程等待其他线程完成
        latch.await();
        System.out.println("All threads have finished.");
    }
}

在这个示例中,CountDownLatch用于等待三个工作线程完成各自的任务后,再让主线程继续执行。

4.2 CyclicBarrier
4.2.1 原理

CyclicBarrier 是一种同步辅助工具,它允许一组线程互相等待,直到所有线程都达到一个共同的屏障点。与CountDownLatch不同的是,CyclicBarrier可以重复使用,即当所有线程都达到屏障后,它可以被重置,以便下次使用。

4.2.2 应用场景

CyclicBarrier 适用于需要多个线程并肩作战、阶段性同步的场景,例如:

  • 并行计算中的分段任务,每个阶段都需要同步
  • 多线程游戏中等待玩家都准备好后再继续
  • 模拟并发用户场景,等待所有用户就绪后一起执行
4.2.3 代码实现与示例
示例1:多线程阶段性同步
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {

    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("All threads reached the barrier, proceeding to the next step.");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
                    barrier.await(); // 等待其他线程
                    System.out.println(Thread.currentThread().getName() + " passed the barrier.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这个示例中,三个线程在执行完各自的任务后,等待其他线程到达屏障点。所有线程都到达后,它们将继续执行下一个任务。

4.3 Semaphore
4.3.1 原理

Semaphore 是一种用于控制同时访问特定资源的线程数量的同步工具。它通过维护一个许可的计数器来控制资源的访问数量,线程在进入临界区前必须获得许可,离开时归还许可。

4.3.2 应用场景

Semaphore 适用于需要限制资源访问数量的场景,例如:

  • 限制数据库连接池的连接数量
  • 控制对某个服务的并发访问
  • 实现限流机制
4.3.3 代码实现与示例
示例1:限制对资源的并发访问
import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    private static final int MAX_CONCURRENT_THREADS = 2;
    private static final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_THREADS);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可
                    System.out.println(Thread.currentThread().getName() + " acquired a permit.");
                    Thread.sleep(2000); // 模拟工作任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + " released a permit.");
                    semaphore.release(); // 释放许可
                }
            }).start();
        }
    }
}

在这个示例中,Semaphore限制了最多只能有两个线程同时访问某个资源,其余线程将等待直到有可用的许可。

4.4 Exchanger
4.4.1 原理

Exchanger 是一种用于在两个线程之间交换数据的同步工具。它提供了一个同步点,两个线程可以在这个点上交换数据。两个线程必须都到达交换点,否则它们将一直等待。

4.4.2 应用场景

Exchanger 适用于需要两个线程之间交换数据的场景,例如:

  • 生产者和消费者之间的数据交换
  • 数据处理的流水线,每个步骤由不同线程执行
  • 在多线程游戏中交换玩家的状态信息
4.4.3 代码实现与示例
示例1:线程之间的数据交换
import java.util.concurrent.Exchanger;

public class ExchangerExample {

    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            String data = "Data from Thread 1";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Thread 1 received: " + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            String data = "Data from Thread 2";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Thread 2 received: " + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

在这个示例中,两个线程分别准备了一段数据,并在同步点上交换了彼此的数据。

第四章小结

本章详细介绍了Java中的几种同步辅助工具,包括CountDownLatchCyclicBarrierSemaphoreExchanger。这些工具在多线程协作中起到了关键作用,帮助开发者有效地管理线程之间的协调和资源的共享。

下一章将介绍Java中的原子类和无锁编程,包括AtomicIntegerAtomicLongAtomicReference等,它们提供了一种高效且线程安全的并发处理方式。
在这里插入图片描述

第五章:原子类和无锁编程

无锁编程是并发编程中的一个重要方向,Java通过java.util.concurrent.atomic包提供了一些原子类,帮助开发者在高并发场景下实现高效的线程安全操作。这些原子类通过CAS(Compare-And-Swap)算法来实现原子操作,无需使用传统的锁机制。

5.1 AtomicIntegerAtomicLong
5.1.1 原理

AtomicIntegerAtomicLong 是提供整数类型原子操作的类。它们内部通过CAS操作保证了对整数的增减、赋值等操作的原子性。这些操作在多线程环境下不会出现竞争条件。

5.1.2 应用场景

AtomicIntegerAtomicLong 适用于需要高效进行整数操作且不希望使用锁的场景,例如:

  • 高并发计数器
  • 基于CAS的乐观锁
  • 实现自定义的原子操作
5.1.3 代码实现与示例
示例1:使用AtomicInteger实现高并发计数器
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {

    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i <

 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    counter.incrementAndGet(); // 原子性递增
                }
            }).start();
        }

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

        System.out.println("Final counter value: " + counter.get());
    }
}

在这个示例中,AtomicInteger确保了多个线程对同一计数器的操作是线程安全的。

5.2 AtomicReference
5.2.1 原理

AtomicReference 是一个可以保证引用类型变量原子性的类。它通过CAS算法保证了对对象引用的更新操作在多线程环境下的安全性。AtomicReference 允许开发者在高并发场景下安全地进行对象的引用更新操作。

5.2.2 应用场景

AtomicReference 适用于需要安全更新共享对象引用的场景,例如:

  • 无锁的对象更新操作
  • 实现不可变对象的替换
  • 在并发环境中安全地交换对象引用
5.2.3 代码实现与示例
示例1:使用AtomicReference实现无锁对象更新
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {

    private static final AtomicReference<String> reference = new AtomicReference<>("initial");

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                String oldValue = reference.get();
                String newValue = oldValue + "-updated";
                if (reference.compareAndSet(oldValue, newValue)) {
                    System.out.println("Updated reference to: " + newValue);
                } else {
                    System.out.println("Failed to update reference.");
                }
            }).start();
        }
    }
}

在这个示例中,AtomicReference通过CAS操作保证了对字符串引用的更新是线程安全的。

第五章小结

本章介绍了Java中的原子类和无锁编程的基本原理与实现。通过使用AtomicIntegerAtomicLongAtomicReference等类,开发者可以在高并发场景下实现高效的线程安全操作,而无需使用传统的锁机制。

下一章将介绍线程池及其应用,包括ThreadPoolExecutorScheduledThreadPoolExecutor,帮助开发者更好地管理多线程环境下的线程资源。
在这里插入图片描述

第六章:线程池及其应用

线程池是Java并发编程中非常重要的工具,它通过复用线程来提高系统的效率,并避免了频繁创建和销毁线程的开销。本章将介绍ThreadPoolExecutorScheduledThreadPoolExecutor的原理、应用场景及代码实现。

6.1 ThreadPoolExecutor
6.1.1 原理

ThreadPoolExecutor 是Java中最强大的线程池实现类。它允许开发者通过配置核心线程数、最大线程数、线程存活时间、任务队列等参数来自定义线程池的行为。

ThreadPoolExecutor 的工作机制如下:

  • 核心线程数:线程池中始终保持存活的线程数量。
  • 最大线程数:线程池中允许的最大线程数量。
  • 任务队列:当核心线程数已满时,新的任务将被放入任务队列中等待执行。
  • 线程存活时间:当线程数超过核心线程数时,多余的线程在空闲一段时间后将被终止。
6.1.2 应用场景

ThreadPoolExecutor 适用于需要动态管理线程的场景,例如:

  • 服务器请求处理
  • 并行任务执行
  • 高并发任务的调度和管理
6.1.3 代码实现与示例
示例1:使用ThreadPoolExecutor管理线程
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorExample {

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // 核心线程数
            5, // 最大线程数
            60, // 空闲线程存活时间
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(10) // 任务队列
        );

        for (int i = 0; i < 15; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is processing task " + index);
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

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

在这个示例中,ThreadPoolExecutor被配置为一个拥有2个核心线程、5个最大线程和一个容量为10的任务队列的线程池。线程池管理着并发任务的执行。

6.2 ScheduledThreadPoolExecutor
6.2.1 原理

ScheduledThreadPoolExecutor 是一个能够调度任务在指定时间或周期性执行的线程池。它扩展了ThreadPoolExecutor,支持任务的定时调度和周期性执行。

6.2.2 应用场景

ScheduledThreadPoolExecutor 适用于需要定时或周期性执行任务的场景,例如:

  • 定时任务调度
  • 周期性数据刷新
  • 定时触发事件
6.2.3 代码实现与示例
示例1:使用ScheduledThreadPoolExecutor实现定时任务
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExecutorExample {

    public static void main(String[] args) {
        ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(2);

        scheduler.scheduleAtFixedRate(() -> {
            System.out.println("Executing scheduled task at " + System.currentTimeMillis());
        }, 1, 3, TimeUnit.SECONDS); // 初始延迟1秒,之后每隔3秒执行一次

        // 在示例中,线程池不会被显式关闭,以便观察定时任务的执行
    }
}

在这个示例中,ScheduledThreadPoolExecutor被配置为每隔3秒执行一次定时任务。

第六章小结

本章介绍了Java中的线程池及其应用。通过使用ThreadPoolExecutorScheduledThreadPoolExecutor,开发者可以有效地管理多线程环境下的线程资源,提升系统的性能和稳定性。

下一章将介绍Fork/Join框架及其在并行计算中的应用,包括如何使用ForkJoinPoolForkJoinTask实现大规模数据的并行处理。
在这里插入图片描述

第七章:Fork/Join框架

Fork/Join框架是Java 7引入的一个并行计算框架,用于将大任务拆分为小任务并行处理,以充分利用多核处理器的计算能力。本章将介绍ForkJoinPoolForkJoinTask的原理、应用场景及代码实现。

7.1 ForkJoinPool
7.1.1 原理

ForkJoinPool 是一个支持并行任务处理的线程池。它实现了工作窃取算法(Work Stealing),即当一个线程完成了自己分配的任务后,它可以窃取其他线程还未完成的任务来执行,以提高整体的任务处理效率。

7.1.2 应用场景

ForkJoinPool 适用于需要将大任务分解为多个子任务并行处理的场景,例如:

  • 大规模数据处理
  • 递归算法的并行化
  • 并行搜索和排序
7.1.3 代码实现与示例
示例1:使用ForkJoinPool实现并行求和
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinSumExample {

    private static class SumTask extends RecursiveTask<Integer> {
        private static final int THRESHOLD = 10;
        private final int[] array;
        private final int start;
        private final int end;

        public SumTask(int[] array, int start, int end) {
            this.array = array;
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= THRESHOLD) {
                int sum = 0;
                for (int i = start; i <= end; i++) {
                    sum += array[i];
                }
                return sum;
            } else {
                int mid = (start + end) / 2;
                SumTask leftTask = new SumTask(array, start, mid);
                SumTask rightTask = new SumTask(array, mid + 1, end);
                leftTask.fork(); // 拆分子任务
                int

 rightResult = rightTask.compute(); // 执行子任务
                int leftResult = leftTask.join(); // 获取左任务结果
                return leftResult + rightResult;
            }
        }
    }

    public static void main(String[] args) {
        int[] array = new int[100];
        for (int i = 0; i < array.length; i++) {
            array[i] = i + 1;
        }

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        SumTask sumTask = new SumTask(array, 0, array.length - 1);
        int result = forkJoinPool.invoke(sumTask);

        System.out.println("Sum: " + result);
    }
}

在这个示例中,ForkJoinPool用于并行求和,将数组的求和任务拆分为多个子任务并行执行。

第七章小结

本章介绍了Java中的Fork/Join框架及其在并行计算中的应用。通过使用ForkJoinPoolForkJoinTask,开发者可以高效地将大任务拆分为小任务并行处理,充分利用多核处理器的计算能力。

下一章将介绍并发集合及其在高并发场景中的应用,包括ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue等。
在这里插入图片描述

第八章:并发集合

并发集合是Java中为应对高并发场景而设计的一系列线程安全的集合类。与传统的集合类不同,并发集合类在实现上充分考虑了多线程环境下的性能优化。本章将介绍ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue的原理、应用场景及代码实现。

8.1 ConcurrentHashMap
8.1.1 原理

ConcurrentHashMap 是Java中的一个线程安全的哈希表实现。它通过分段锁机制和CAS操作保证了在多线程环境下的高效读写操作。ConcurrentHashMap将整个哈希表分成多个段,每个段有自己的锁,这样在进行并发写操作时,只需锁定其中一个段,而不是锁定整个哈希表,从而提高了并发性能。

8.1.2 应用场景

ConcurrentHashMap 适用于需要高效读取和更新映射数据的高并发场景,例如:

  • 线程安全的缓存实现
  • 高并发访问的配置管理
  • 并发统计和分析
8.1.3 代码实现与示例
示例1:使用ConcurrentHashMap实现线程安全的缓存
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

    private static final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        cache.put("key1", "value1");
        cache.put("key2", "value2");

        Runnable task = () -> {
            String value = cache.get("key1");
            System.out.println(Thread.currentThread().getName() + " read value: " + value);
            cache.put("key1", value + "-updated");
        };

        // 启动多个线程并发访问缓存
        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}

在这个示例中,ConcurrentHashMap确保了多个线程并发访问和更新缓存数据的线程安全性。

8.2 CopyOnWriteArrayList
8.2.1 原理

CopyOnWriteArrayList 是一个线程安全的、基于数组的列表实现。它的核心思想是写时复制(Copy-On-Write),即在进行写操作时,会创建一个新的数组副本,修改完副本后再将其替换为原数组。这种机制确保了读操作的高效性,因为读操作无需加锁。

8.2.2 应用场景

CopyOnWriteArrayList 适用于读多写少的场景,例如:

  • 配置数据的读取
  • 事件监听器的管理
  • 需要频繁读取而修改较少的列表数据
8.2.3 代码实现与示例
示例1:使用CopyOnWriteArrayList实现线程安全的事件监听器管理
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {

    private static final List<String> listeners = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        listeners.add("Listener1");
        listeners.add("Listener2");

        Runnable readTask = () -> {
            for (String listener : listeners) {
                System.out.println(Thread.currentThread().getName() + " notified " + listener);
            }
        };

        Runnable writeTask = () -> {
            listeners.add("Listener3");
            System.out.println(Thread.currentThread().getName() + " added Listener3");
        };

        // 启动多个读线程和写线程
        for (int i = 0; i < 3; i++) {
            new Thread(readTask).start();
            new Thread(writeTask).start();
        }
    }
}

在这个示例中,CopyOnWriteArrayList确保了在多个线程并发读取和修改监听器列表时的线程安全性。

8.3 ConcurrentLinkedQueue
8.3.1 原理

ConcurrentLinkedQueue 是一个无锁的线程安全队列,它基于非阻塞算法实现,通过CAS操作来保证队列的高效并发性。ConcurrentLinkedQueue是一个先进先出的队列,适用于多线程环境下的任务队列或消息队列。

8.3.2 应用场景

ConcurrentLinkedQueue 适用于需要高效并发访问的队列场景,例如:

  • 多线程任务调度
  • 高并发的消息处理
  • 非阻塞的任务队列
8.3.3 代码实现与示例
示例1:使用ConcurrentLinkedQueue实现并发任务调度
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueExample {

    private static final ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

    public static void main(String[] args) {
        queue.add("Task1");
        queue.add("Task2");

        Runnable worker = () -> {
            String task;
            while ((task = queue.poll()) != null) {
                System.out.println(Thread.currentThread().getName() + " processed " + task);
            }
        };

        // 启动多个线程处理队列中的任务
        for (int i = 0; i < 3; i++) {
            new Thread(worker).start();
        }
    }
}

在这个示例中,ConcurrentLinkedQueue确保了多个线程并发处理任务时的高效性和线程安全性。

第八章小结

本章详细介绍了Java中的并发集合及其在高并发场景中的应用。通过使用ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue,开发者可以在多线程环境下实现高效的集合操作,同时确保线程安全性。

下一章将介绍CompletableFuture与异步编程,以及如何使用CompletableFuture来实现复杂的异步任务处理。
在这里插入图片描述

第九章:CompletableFuture与异步编程

在现代应用程序中,异步编程已经成为一种常见的并发处理方式。Java 8引入的CompletableFuture类提供了一种强大的方式来编写异步代码,它不仅支持非阻塞的异步操作,还可以进行复杂的任务组合和处理。本章将详细介绍CompletableFuture的原理、应用场景及代码实现。

9.1 CompletableFuture的基础概念
9.1.1 原理

CompletableFutureFuture接口的扩展,它提供了更多的功能,使得编写异步代码更加简洁和强大。与传统的Future不同,CompletableFuture不仅可以处理单个异步任务,还可以将多个任务组合在一起,形成更复杂的异步操作流。

CompletableFuture主要依赖于以下几个核心概念:

  • 非阻塞异步操作:通过异步计算,避免阻塞主线程,从而提高应用程序的响应速度。
  • 任务组合:可以将多个异步任务组合成一个复杂的异步操作流。
  • 异常处理:支持对异步操作中的异常进行处理和恢复。
9.1.2 应用场景

CompletableFuture 适用于以下场景:

  • 并行处理多个异步任务,并将结果汇总
  • 异步调用远程服务,并根据结果执行后续操作
  • 实现复杂的异步操作流,如依赖关系、任务组合、异常处理等
9.2 CompletableFuture的创建与执行
9.2.1 创建异步任务

CompletableFuture 提供了多种方式来创建异步任务,例如runAsync()supplyAsync()方法。

示例1:创建简单的异步任务
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {

    public static void main(String[] args) {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("Executing task in " + Thread.currentThread().getName());
        });

        // 等待异步任务完成
        future.join();
    }
}

在这个示例中,runAsync()方法创建了一个不返回结果的异步任务。异步任务在另一个线程中执行,并且不会阻塞主线程。

9.2.2 处理异步任务的结果

CompletableFuture 提供了thenApply()thenAccept()thenRun()等方法,用于在异步任务完成后对结果进行处理。

示例2:处理异步任务的返回结果
import java.util.concurrent.CompletableFuture;

public class CompletableFutureResultExample {

    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Calculating in " + Thread.currentThread().getName());
            return 42;
        });

        future.thenApply(result -> {
            System.out.println("Result: " + result);
            return result * 2;
        }).thenAccept(result -> {
            System.out.println("Processed Result: " + result);
        });

        // 等待所有异步操作完成
        future.join();
    }
}

在这个示例中,supplyAsync()创建了一个返回结果的异步任务。然后通过thenApply()对结果进行处理,最后通过thenAccept()消费处理后的结果。

9.3 组合多个CompletableFuture
9.3.1 并行执行任务

CompletableFuture 支持将多个异步任务组合在一起并行执行,并可以使用thenCombine()方法合并任务的结果。

示例3:并行执行和合并结果
import java.util.concurrent.CompletableFuture;

public class CompletableFutureCombineExample {

    public static void main(String[] args) {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            return 10;
        });

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            return 20;
        });

        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        });

        System.out.println("Combined Result: " + combinedFuture.join());
    }
}

在这个示例中,两个异步任务并行执行,并在完成后通过thenCombine()方法将结果合并。

9.3.2 处理异步任务的依赖关系

在某些情况下,任务B依赖于任务A的结果,CompletableFuture提供了thenCompose()方法来处理这种依赖关系。

示例4:任务依赖关系的处理
import java.util.concurrent.CompletableFuture;

public class CompletableFutureComposeExample {

    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Hello";
        }).thenCompose(result -> {
            return CompletableFuture.supplyAsync(() -> {
                return result + " World";
            });
        });

        System.out.println("Final Result: " + future.join());
    }
}

在这个示例中,第二个任务依赖于第一个任务的结果,使用thenCompose()方法将它们组合在一起。

9.4 处理异步操作中的异常

CompletableFuture 支持对异步操作中的异常进行处理,可以使用exceptionally()方法来处理异常并提供默认值。

示例5:处理异步任务中的异常
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionExample {

    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() > 0.5) {
                throw new RuntimeException("Something went wrong");
            }
            return 42;
        }).exceptionally(ex -> {
            System.out.println("Caught exception: " + ex.getMessage());
            return -1;
        });

        System.out.println("Result: " + future.join());
    }
}

在这个示例中,如果异步任务抛出异常,exceptionally()方法会捕获异常并返回一个默认值。

9.5 CompletableFuture的高级应用
9.5.1 超时处理

CompletableFuture 提供了orTimeout()completeOnTimeout()方法,允许开发者设置超时处理机制。

示例6:处理超时的异步任务
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureTimeoutExample {

    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 42;
        }).orTimeout(2, TimeUnit.SECONDS)
          .exceptionally(ex -> {
              System.out.println("Task timed out.");
              return -1;
          });

        System.out.println("Result: " + future.join());
    }
}

在这个示例中,如果异步任务没有在2秒内完成,orTimeout()方法会触发超时处理,并返回默认值。

第九章小结

本章详细介绍了CompletableFuture及其在异步编程中的应用。通过使用CompletableFuture,开发者可以编写非阻塞的异步代码,并处理复杂的任务组合和异常情况。它为现代应用程序的并发处理提供了强大的工具支持。

下一章将探讨JUC中的高级特性,包括自定义同步器(AbstractQueuedSynchronizer,AQS)、ThreadLocal的使用等,帮助开发者掌握更深入的并发编程技巧。
在这里插入图片描述

第十章:JUC高级特性

在前面的章节中,我们讨论了JUC中的各种工具和机制,帮助开发者在并发编程中高效、安全地管理多线程操作。本章将深入探讨JUC中的一些高级特性,包括自定义同步器(AbstractQueuedSynchronizer,AQS)、ThreadLocal的使用等。

10.1 自定义同步器(AQS)
10.1.1 原理

AbstractQueuedSynchronizer(AQS)是JUC中的一个基础框架,用于构建自定义的同步器,如锁、信号量、倒计时器等。AQS基于FIFO队列实现线程的排队和管理,提供了独占模式和共享模式两种模式来支持不同的同步需求。

  • 独占模式:一个线程独占资源,其他线程需要等待。
  • 共享模式:多个线程可以共享资源,通常用于实现信号量。
10.1.2 应用场景

AQS主要用于构建自定义的同步器,如:

  • 实现自定义的锁
  • 创建自定义的同步工具,如自定义的倒计时器或栅栏
  • 实现基于AQS的高级并发控制器
10.1.3 代码实现与示例
示例1:基于AQS实现一个简单的自定义锁
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CustomLock {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        public boolean isLocked() {
            return isHeldExclusively();
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public boolean isLocked() {
        return sync.isLocked();
    }

    public static void main(String[] args) {
        CustomLock lock = new CustomLock();
        Runnable task = () -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        };

        new Thread(task).start();
        new Thread(task).start();
    }
}

在这个示例中,我们使用AQS实现了一个简单的自定义锁,允许多个线程竞争锁,并通过FIFO队列管理线程的等待和调度。

10.2 ThreadLocal
10.2.1 原理

ThreadLocal 是Java中的一个工具类,用于在多线程环境下为每个线程维护独立的变量副本。它通过为每个线程创建一个单独的副本,避免了线程间共享数据导致的并发问题。ThreadLocal通常用于为线程提供独立的上下文信息,如用户会话、事务处理等。

10.2.2 应用场景

ThreadLocal 适用于以下场景:

  • 每个线程需要一个独立的变量副本
  • 需要在线程间隔离共享变量
  • 为每个线程提供独立的上下文信息
10.2.3 代码实现与示例
示例2:使用ThreadLocal管理用户会话
public class ThreadLocalExample {

    private static final ThreadLocal<String> userSession = ThreadLocal.withInitial(() -> "Default User");

    public static void main(String[] args) {
        Runnable task1 = () -> {
            userSession.set("User1");
            System.out.println(Thread.currentThread().getName() + " session: " + userSession.get());
        };

        Runnable task2 = () -> {
            userSession.set("User2");
            System.out.println(Thread.currentThread().getName() + " session: " + userSession.get());
        };

        new Thread(task1).start();
        new Thread(task2).start();
    }
}

在这个示例中,ThreadLocal为每个线程维护了独立的用户会话信息,避免了线程间数据共享导致的并发问题。

第十章小结

本章探讨了JUC中的一些高级特性,包括基于AQS的自定义同步器和ThreadLocal的使用。通过这些高级特性,开发者可以在更复杂的并发场景中构建自定义的同步工具,并为每个线程提供独立的上下文信息。

至此,这本关于Java工程师面试中JUC相关知识点的书籍已经涵盖了从基础到高级的各种内容,帮助读者全面掌握并发编程的核心知识与技巧。无论是在面试中还是在实际开发中,这些知识都将是你成为优秀Java工程师的重要工具。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的代码

如果您有受益,欢迎打赏博主😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值