Java多线程终极指南:从基础到高级应用

一、多线程基础概念

1.1 进程与线程的区别

对比维度进程(Process)线程(Thread)
定义操作系统资源分配的基本单位CPU调度的基本单位
内存空间独立内存空间共享所属进程的内存空间
通信方式进程间通信(IPC)较复杂可直接读写共享变量
创建开销大(需要分配独立资源)小(共享进程资源)
稳定性一个进程崩溃不影响其他进程一个线程崩溃可能导致整个进程退出
进程(Process)

在Java中,进程是操作系统资源分配的基本单位,具有独立的内存空间。每个Java应用程序运行时都至少有一个进程。进程特点包括:

  • 独立性:拥有独立的地址空间、数据栈等
  • 资源开销大:创建和销毁需要较多系统资源
  • 通信复杂:进程间通信(IPC)需要特殊机制(如管道、套接字等)
线程(Thread)

线程是Java并发编程的基本执行单元,是进程内的一个独立执行流。特点包括:

  • 共享进程资源:同一进程内的线程共享堆内存和方法区
  • 轻量级:创建和切换开销远小于进程
  • 通信简单:可通过共享变量直接通信
  • Java通过java.lang.Thread类和Runnable接口实现多线程
进程 vs 线程:餐厅比喻

想象一家餐厅:

  • 进程就像整个餐厅,有独立的厨房(内存)、收银台(资源)
  • 线程就像餐厅里的服务员,多个服务员共享同一个厨房和收银台

1.2 为什么需要多线程

  1. 提高CPU利用率:当线程I/O阻塞时,其他线程可以继续使用CPU
  2. 更快的响应:GUI程序使用单独线程处理用户输入
  3. 简化建模:每个线程处理单一任务,代码更清晰
  4. 多核优势:现代CPU多核心可真正并行执行线程

日常例子:浏览器同时下载多个文件(每个下载任务一个线程),同时还能响应用户操作(UI线程)。

二、Java线程创建与管理

2.1 创建线程的三种方式

方式1:继承Thread类
// 自定义线程类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行: " + Thread.currentThread().getName());
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 启动线程
    }
}
方式2:实现Runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable线程: " + Thread.currentThread().getName());
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}
方式3:使用Callable和Future(可获取返回值)
import java.util.concurrent.*;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "Callable结果";
    }
}

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());
        
        System.out.println("等待结果...");
        String result = future.get();  // 阻塞直到获取结果
        System.out.println("获取结果: " + result);
        
        executor.shutdown();
    }
}

2.2 三种创建方式对比

对比点继承Thread类实现Runnable接口实现Callable接口
返回值
异常处理只能在run()内处理只能在run()内处理可以通过Future获取
单继承限制受限于Java单继承不受限不受限
线程池支持不支持支持支持
适用场景简单线程任务推荐方式需要返回结果的场景

建议:优先选择实现Runnable接口或Callable接口的方式,避免继承的局限性。

三、线程生命周期与状态转换

3.1 线程的6种状态

Java线程在生命周期中有6种状态(定义在Thread.State枚举中):

  1. NEW(新建):线程被创建但尚未启动
  2. RUNNABLE(可运行):线程正在JVM中执行或等待操作系统资源
  3. BLOCKED(阻塞):等待监视器锁(进入synchronized块)
  4. WAITING(等待):无限期等待其他线程执行特定操作(如wait())
  5. TIMED_WAITING(计时等待):有限时间等待(如sleep())
  6. TERMINATED(终止):线程执行完毕

3.2 状态转换图

NEW ---start()---> RUNNABLE
RUNNABLE ---获取锁---> BLOCKED
RUNNABLE ---wait()---> WAITING
RUNNABLE ---sleep()---> TIMED_WAITING
WAITING ---notify()---> RUNNABLE
TIMED_WAITING ---时间到---> RUNNABLE
RUNNABLE ---run()结束---> TERMINATED

代码示例观察状态

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("新建状态: " + thread.getState());  // NEW
        
        thread.start();
        System.out.println("启动后状态: " + thread.getState());  // RUNNABLE
        
        Thread.sleep(100);
        System.out.println("sleep时状态: " + thread.getState());  // TIMED_WAITING
        
        thread.join();
        System.out.println("结束后状态: " + thread.getState());  // TERMINATED
    }
}

四、线程同步与锁机制

4.1 同步问题的产生

概念:当多个线程访问共享资源时,由于线程调度的不确定性,可能导致:

  • 竞态条件(Race Condition):执行结果依赖于线程执行的时序
  • 内存可见性问题:线程对共享变量的修改对其他线程不可见
  • 指令重排序:编译器和处理器优化导致的执行顺序改变

例如:这就像你和室友共用一个冰箱:

  • 你看到最后一瓶可乐(检查条件)
  • 你伸手去拿(执行操作)
  • 同时你室友也伸手
  • 结果要么:1) 你俩各拿到半瓶 2) 系统崩溃 3) 可乐凭空消失

经典问题:银行取款问题

class BankAccount {
    private int balance = 1000;
    
    public void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(10);  // 模拟处理时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额: " + balance);
        } else {
            System.out.println("余额不足");
        }
    }
}

public class BankDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        
        // 两个线程同时取款
        new Thread(() -> account.withdraw(800), "线程1").start();
        new Thread(() -> account.withdraw(800), "线程2").start();
    }
}

输出可能

线程1取款800,余额: 200
线程2取款800,余额: -600

4.2 同步解决方案

方案1:synchronized关键字

特性

  • 内置锁(Intrinsic Lock)/监视器锁(Monitor Lock)
  • 保证原子性(atomicity)和可见性(visibility)
  • 可重入性(Reentrancy):线程可以重复获取已持有的锁
  • 方法级和代码块级两种使用方式

流程

  1. 线程到达同步代码:“我要进这个房间”
  2. JVM门神:“请出示你的锁对象身份证”
  3. 如果没人占用:“请进,记得出来时敲门”
  4. 如果已被占用:“门口排队,别踢门!”
public class house {
    private final Object lock = new Object();
    
    public void use() {
        synchronized(lock) {  // 获取锁
            // 临界区代码
            System.out.println("正在使用中...");
        }  // 释放锁
    }
    
    public synchronized void clean() {  // 方法级同步
        System.out.println("保洁阿姨工作中");
    }
}
方案2:ReentrantLock

特性

  • 可重入
  • 可中断(lockInterruptibly)
  • 尝试获取锁(tryLock)
  • 公平/非公平模式
import java.util.concurrent.locks.ReentrantLock;

class BankAccount {
    private final ReentrantLock lock = new ReentrantLock();
    private int balance = 1000;
    
    public void withdraw(int amount) {
        lock.lock();  // 加锁
        try {
            if (balance >= amount) {
                Thread.sleep(10);
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额: " + balance);
            } else {
                System.out.println("余额不足");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  // 确保释放锁
        }
    }
}

4.3 synchronized与ReentrantLock对比

对比点synchronizedReentrantLock
实现机制JVM层面实现JDK代码实现
锁获取方式自动获取释放需要手动lock/unlock
灵活性相对不灵活可尝试获取锁、定时锁、公平锁等
性能优化后性能接近高竞争下性能更好
中断响应不支持支持lockInterruptibly()
条件队列单一可创建多个Condition
适用场景简单同步需求复杂同步控制

五、线程间通信

5.1 wait/notify机制

生产者-消费者模型

class MessageQueue {
    private String message;
    private boolean empty = true;
    
    public synchronized String take() {
        while (empty) {
            try {
                wait();  // 等待消息
            } catch (InterruptedException e) {}
        }
        empty = true;
        notifyAll();  // 通知生产者
        return message;
    }
    
    public synchronized void put(String message) {
        while (!empty) {
            try {
                wait();  // 等待消费
            } catch (InterruptedException e) {}
        }
        empty = false;
        this.message = message;
        notifyAll();  // 通知消费者
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue();
        
        // 生产者
        new Thread(() -> {
            String[] messages = {"消息1", "消息2", "消息3"};
            for (String msg : messages) {
                queue.put(msg);
                System.out.println("生产: " + msg);
            }
        }).start();
        
        // 消费者
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                String msg = queue.take();
                System.out.println("消费: " + msg);
            }
        }).start();
    }
}

5.2 Condition接口

import java.util.concurrent.locks.*;

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();
    
    final Object[] items = new Object[100];
    int putptr, takeptr, count;
    
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();  // 等待不满
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();  // 通知不空
        } finally {
            lock.unlock();
        }
    }
    
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();  // 等待不空
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();  // 通知不满
            return x;
        } finally {
            lock.unlock();
        }
    }
}

六、线程池与Executor框架

6.1 为什么使用线程池

  1. 降低资源消耗:重复利用已创建的线程
  2. 提高响应速度:任务到达时线程已存在
  3. 提高线程可管理性:统一分配、调优和监控
  4. 防止资源耗尽:限制最大线程数

6.2 线程池核心参数

参数说明
corePoolSize核心线程数,即使空闲也不会被回收
maximumPoolSize最大线程数,当工作队列满时创建新线程直到达到此数量
keepAliveTime非核心线程空闲存活时间
unit存活时间单位
workQueue工作队列,保存等待执行的任务
threadFactory线程工厂,用于创建新线程
handler拒绝策略,当线程池和工作队列都满时如何处理新任务

6.3 四种常见线程池

// 1. 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 2. 单线程池(保证顺序执行)
ExecutorService singleThread = Executors.newSingleThreadExecutor();

// 3. 缓存线程池(自动扩容)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 4. 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);

6.4 自定义线程池示例

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,  // 核心线程数
            4,  // 最大线程数
            60, // 空闲时间
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2),  // 任务队列容量2
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
        );
        
        // 提交10个任务
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executor.shutdown();
    }
}

6.5 线程池拒绝策略

策略行为
AbortPolicy默认策略,直接抛出RejectedExecutionException
CallerRunsPolicy用调用者所在线程来执行任务
DiscardPolicy直接丢弃任务,不做任何处理
DiscardOldestPolicy丢弃队列中最旧的任务,然后尝试提交当前任务

七、高级并发工具类

7.1 CountDownLatch

应用场景:多个线程等待直到所有前置操作完成

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);  // 需要计数3次
        
        new Thread(() -> {
            System.out.println("任务1完成");
            latch.countDown();  // 计数减1
        }).start();
        
        new Thread(() -> {
            System.out.println("任务2完成");
            latch.countDown();
        }).start();
        
        new Thread(() -> {
            System.out.println("任务3完成");
            latch.countDown();
        }).start();
        
        latch.await();  // 等待计数归零
        System.out.println("所有任务完成,继续主线程");
    }
}

7.2 CyclicBarrier

应用场景:一组线程互相等待到达屏障点

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有线程到达屏障,执行屏障动作");
        });
        
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "到达屏障");
                try {
                    barrier.await();  // 等待其他线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "继续执行");
            }).start();
        }
    }
}

7.3 Semaphore

应用场景:控制同时访问特定资源的线程数量

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);  // 允许3个线程同时访问
        
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();  // 获取许可
                    System.out.println(Thread.currentThread().getName() + "获得许可,执行中...");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "释放许可");
                    semaphore.release();  // 释放许可
                }
            }, "线程" + i).start();
        }
    }
}

7.4 并发工具对比

工具类作用关键方法可重用性
CountDownLatch一个或多个线程等待其他线程完成操作countDown(), await()
CyclicBarrier一组线程互相等待到达屏障点await()
Semaphore控制同时访问特定资源的线程数量acquire(), release()
Phaser更灵活的屏障,可以动态注册和注销参与方arrive(), awaitAdvance()

八、原子变量与CAS

8.1 原子操作类

Java提供了一系列原子变量类,如AtomicInteger, AtomicLong, AtomicReference等。

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();  // 原子递增
            }
        };
        
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终计数: " + counter.get());  // 总是2000
    }
}

8.2 CAS原理

CAS(Compare And Swap)是原子变量的实现原理,包含三个操作数:

  • 内存位置(V)
  • 预期原值(A)
  • 新值(B)

当且仅当V的值等于A时,处理器才会用B更新V的值,否则不执行更新。

ABA问题:虽然值还是A,但可能已经被修改过又改回来了。解决方案:使用AtomicStampedReference带版本号。

九、并发集合类

9.1 常用并发集合

接口非线程安全实现线程安全实现
ListArrayListCopyOnWriteArrayList
SetHashSetCopyOnWriteArraySet, ConcurrentSkipListSet
MapHashMapConcurrentHashMap, ConcurrentSkipListMap
QueueLinkedListArrayBlockingQueue, LinkedBlockingQueue
DequeArrayDequeLinkedBlockingDeque

9.2 ConcurrentHashMap示例

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // 多个线程并发写入
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    String key = "key-" + taskId + "-" + j;
                    map.put(key, j);
                }
            });
        }
        
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Map大小: " + map.size());
    }
}

9.3 CopyOnWriteArrayList示例

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        
        // 一个线程迭代
        new Thread(() -> {
            list.add("A");
            list.add("B");
            Iterator<String> it = list.iterator();
            while (it.hasNext()) {
                System.out.println("迭代: " + it.next());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // 另一个线程修改
        new Thread(() -> {
            try {
                Thread.sleep(500);
                list.add("C");
                System.out.println("添加了C");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

十、Java内存模型(JMM)与happens-before

10.1 JMM核心概念

Java内存模型定义了线程如何与内存交互,主要解决以下问题:

  1. 原子性:基本读写操作是原子的
  2. 可见性:一个线程修改对另一个线程可见
  3. 有序性:防止指令重排序

10.2 happens-before原则

  1. 程序顺序规则:同一线程中的操作,前面的happens-before后面的
  2. 锁规则:解锁happens-before后续加锁
  3. volatile规则:写happens-before后续读
  4. 线程启动规则:线程start()happens-before它的任何操作
  5. 线程终止规则:线程的所有操作happens-before它的终止检测
  6. 中断规则:调用interrupt()happens-before检测到中断
  7. 终结器规则:对象构造happens-before它的finalize()
  8. 传递性:A hb B,B hb C ⇒ A hb C

10.3 volatile关键字

public class VolatileDemo {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;
    }
    
    public void run() {
        while (running) {
            // 工作代码
        }
        System.out.println("线程停止");
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();
        new Thread(demo::run).start();
        Thread.sleep(1000);
        demo.stop();
    }
}

十一、实战案例分析

11.1 高性能计数器

public class HighPerformanceCounter {
    private final AtomicLong counter = new AtomicLong(0);
    private final LongAdder fastCounter = new LongAdder();
    
    // 简单原子计数器
    public void incrementAtomic() {
        counter.incrementAndGet();
    }
    
    // 高并发优化计数器
    public void incrementAdder() {
        fastCounter.increment();
    }
    
    public long getAtomicCount() {
        return counter.get();
    }
    
    public long getAdderCount() {
        return fastCounter.sum();
    }
    
    public static void main(String[] args) throws InterruptedException {
        HighPerformanceCounter counter = new HighPerformanceCounter();
        
        ExecutorService executor = Executors.newFixedThreadPool(8);
        long start = System.currentTimeMillis();
        
        // 测试AtomicLong性能
        for (int i = 0; i < 1000000; i++) {
            executor.execute(counter::incrementAtomic);
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long atomicTime = System.currentTimeMillis() - start;
        
        executor = Executors.newFixedThreadPool(8);
        start = System.currentTimeMillis();
        
        // 测试LongAdder性能
        for (int i = 0; i < 1000000; i++) {
            executor.execute(counter::incrementAdder);
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long adderTime = System.currentTimeMillis() - start;
        
        System.out.println("AtomicLong结果: " + counter.getAtomicCount() + ", 耗时: " + atomicTime + "ms");
        System.out.println("LongAdder结果: " + counter.getAdderCount() + ", 耗时: " + adderTime + "ms");
    }
}

11.2 限流器实现

public class RateLimiter {
    private final Semaphore semaphore;
    private final int maxPermits;
    private final long period;
    private ScheduledExecutorService scheduler;
    
    public RateLimiter(int permits, long period, TimeUnit unit) {
        this.semaphore = new Semaphore(permits);
        this.maxPermits = permits;
        this.period = unit.toMillis(period);
        this.scheduler = Executors.newScheduledThreadPool(1);
        
        scheduler.scheduleAtFixedRate(() -> {
            int current = semaphore.availablePermits();
            if (current < maxPermits) {
                semaphore.release(maxPermits - current);
            }
        }, 0, this.period, TimeUnit.MILLISECONDS);
    }
    
    public boolean tryAcquire() {
        return semaphore.tryAcquire();
    }
    
    public void acquire() throws InterruptedException {
        semaphore.acquire();
    }
    
    public void shutdown() {
        scheduler.shutdown();
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 每秒最多5个请求
        RateLimiter limiter = new RateLimiter(5, 1, TimeUnit.SECONDS);
        
        // 模拟10个请求
        for (int i = 1; i <= 10; i++) {
            if (limiter.tryAcquire()) {
                System.out.println("处理请求 " + i);
            } else {
                System.out.println("限流请求 " + i);
            }
            Thread.sleep(100);
        }
        
        limiter.shutdown();
    }
}

十二、常见问题与最佳实践

12.1 多线程常见问题

  1. 死锁:多个线程互相等待对方释放锁

    • 避免方法:按固定顺序获取锁,使用tryLock()设置超时
  2. 活锁:线程不断改变状态但无法继续执行

    • 避免方法:引入随机性,如随机等待时间
  3. 线程饥饿:某些线程长期得不到执行

    • 解决方法:使用公平锁,合理设置线程优先级

12.2 最佳实践

  1. 尽量使用高层并发工具:如线程池、并发集合
  2. 优先使用不可变对象:避免同步问题
  3. 缩小同步范围:只同步必要的代码块
  4. 避免过早优化:先保证正确性,再考虑性能
  5. 考虑使用并行流:Java 8+的parallelStream()
  6. 合理设置线程池大小
    • CPU密集型:CPU核心数+1
    • IO密集型:CPU核心数 × (1 + 平均等待时间/平均计算时间)

12.3 性能调优建议

  1. 减少锁竞争

    • 缩小同步块
    • 使用读写锁(ReentrantReadWriteLock)
    • 使用分段锁(如ConcurrentHashMap)
  2. 避免上下文切换

    • 合理设置线程数
    • 使用协程(如Quasar库)
  3. 使用无锁数据结构

    • Atomic类
    • LongAdder
    • ConcurrentLinkedQueue

总结

Java多线程编程是Java高级开发的核心技能之一。本文从基础概念到高级应用,全面介绍了Java多线程的各个方面:

  1. 线程创建与生命周期管理
  2. 同步机制与锁优化
  3. 线程间通信方式
  4. 线程池与Executor框架
  5. 高级并发工具类
  6. 原子变量与CAS
  7. 并发集合类
  8. Java内存模型
  9. 实战案例与最佳实践

Java 多线程像一群疯跑的小怪兽,协调好就齐力通关,没控制住,程序直接被它们折腾得 “脑震荡”!

收藏转发的人,2024年必暴富!——来自一位贫穷但真诚的博主。

喜欢的点个关注,想了解更多的可以关注微信公众号:“Eric的技术杂货库”,提供更多的干货以及资料下载保存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Clf丶忆笙

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值