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

锁的艺术:从Mutex到ReentrantLock,掌握并发编程的脉搏

锁的艺术:从Mutex到ReentrantLock,掌握并发编程的脉搏


🌟 嗨,我是IRpickstars!

🌌 总有一行代码,能点亮万千星辰。

🔍 在技术的宇宙中,我愿做永不停歇的探索者。

✨ 用代码丈量世界,用算法解码未来。我是摘星人,也是造梦者。

🚀 每一次编译都是新的征程,每一个bug都是未解的谜题。让我们携手,在0和1的星河中,书写属于开发者的浪漫诗篇。


目录

锁的艺术:从Mutex到ReentrantLock,掌握并发编程的脉搏

摘要

锁机制基础概念

什么是锁

锁的分类

从Mutex到ReentrantLock的演进

1. 传统Mutex锁

2. Java synchronized关键字

3. ReentrantLock显式锁

锁机制内部原理深度解析

synchronized实现原理

ReentrantLock基于AQS的实现

性能对比分析

不同锁机制特性对比

性能基准测试

性能测试结果分析

死锁问题与解决方案

死锁产生的四个必要条件

死锁场景代码复现

死锁形成过程时序图

死锁解决方案

1. 顺序锁定法

2. 超时锁定法

实际应用场景

1. 生产者-消费者模式

2. 读写锁在缓存系统中的应用

最佳实践与性能调优

锁使用最佳实践

性能调优策略

总结与展望


摘要

作为一名在并发编程领域深耕多年的开发者,我深刻体会到锁机制在现代多线程编程中的核心地位。从最初接触pthread的mutex互斥锁,到后来深入研究Java的synchronized关键字,再到现在熟练运用ReentrantLock等高级同步工具,这一路走来让我对并发编程有了更深层次的理解。

在我看来,锁不仅仅是保证线程安全的工具,更是一门需要精雕细琢的艺术。每一种锁机制都有其独特的设计哲学和适用场景,就像画家手中的不同画笔,只有深入理解其特性,才能在合适的场景下发挥最大的效用。我曾经在一个高并发的电商系统中,仅仅通过优化锁的选择和使用策略,就将系统的吞吐量提升了40%,这让我更加坚信掌握锁机制的重要性。

近年来,随着多核处理器的普及和分布式系统的兴起,并发编程已经从一个可选技能变成了每个程序员的必备技能。然而,我在技术交流和代码Review中发现,很多开发者对锁的理解还停留在表面,经常出现滥用synchronized、忽视死锁风险、不了解锁升级机制等问题。这些问题在高并发场景下会被无限放大,最终导致系统性能瓶颈甚至稳定性问题。

本文将从我多年的实践经验出发,系统性地梳理从传统Mutex到现代ReentrantLock的演进历程,深入分析各种锁机制的内部原理、性能特征和最佳实践。我希望通过这篇文章,能够帮助读者构建完整的并发编程知识体系,在面对复杂的多线程场景时能够游刃有余。

锁机制基础概念

什么是锁

锁是并发编程中用于控制对共享资源访问的同步原语。它通过互斥机制确保在任意时刻只有一个线程能够访问临界区,从而避免竞态条件的发生。

图1:锁的基本工作原理图

锁的分类

根据不同的维度,锁可以分为多种类型:

按互斥性分类:

  • 互斥锁(Mutex):同时只允许一个线程访问
  • 读写锁(ReadWriteLock):允许多个读线程或一个写线程

按公平性分类:

  • 公平锁:按照申请顺序获得锁
  • 非公平锁:不保证获取顺序,性能更好

按可重入性分类:

  • 可重入锁:同一线程可多次获取
  • 不可重入锁:同一线程重复获取会死锁

从Mutex到ReentrantLock的演进

1. 传统Mutex锁

Mutex是最基本的互斥锁实现,广泛应用于C/C++等系统级编程语言中。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;void* worker_thread(void* arg) {int thread_id = *(int*)arg;for (int i = 0; i < 1000; i++) {// 获取锁pthread_mutex_lock(&mutex);// 临界区:修改共享变量int temp = shared_counter;usleep(1); // 模拟处理时间shared_counter = temp + 1;// 释放锁pthread_mutex_unlock(&mutex);}printf("Thread %d finished\n", thread_id);return NULL;
}int main() {pthread_t threads[3];int thread_ids[3] = {1, 2, 3};// 创建线程for (int i = 0; i < 3; i++) {pthread_create(&threads[i], NULL, worker_thread, &thread_ids[i]);}// 等待线程结束for (int i = 0; i < 3; i++) {pthread_join(threads[i], NULL);}printf("Final counter value: %d\n", shared_counter);pthread_mutex_destroy(&mutex);return 0;
}

2. Java synchronized关键字

Java的synchronized提供了更高层次的锁抽象,简化了多线程编程。

public class SynchronizedExample {private int counter = 0;// 方法级同步public synchronized void increment() {counter++;}// 代码块同步public void incrementBlock() {synchronized(this) {counter++;}}// 静态方法同步public static synchronized void staticMethod() {System.out.println("Static synchronized method");}public synchronized int getCounter() {return counter;}public static void main(String[] args) throws InterruptedException {SynchronizedExample example = new SynchronizedExample();// 创建多个线程进行测试Thread[] threads = new Thread[10];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++) {example.increment();}});}// 启动所有线程for (Thread thread : threads) {thread.start();}// 等待所有线程完成for (Thread thread : threads) {thread.join();}System.out.println("Final counter: " + example.getCounter());// 预期输出:Final counter: 10000}
}

3. ReentrantLock显式锁

ReentrantLock提供了比synchronized更丰富的功能,包括公平性控制、超时机制、中断响应等。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock(true); // 公平锁private int counter = 0;public void increment() {lock.lock();try {counter++;// 模拟业务处理Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public boolean tryIncrementWithTimeout() {try {// 尝试在2秒内获取锁if (lock.tryLock(2, TimeUnit.SECONDS)) {try {counter++;return true;} finally {lock.unlock();}}} catch (InterruptedException e) {Thread.currentThread().interrupt();}return false;}public int getCounter() {lock.lock();try {return counter;} finally {lock.unlock();}}// 演示可重入特性public void reentrantExample() {lock.lock();try {System.out.println("First lock acquired");nestedMethod();} finally {lock.unlock();}}private void nestedMethod() {lock.lock();try {System.out.println("Nested lock acquired");} finally {lock.unlock();}}
}

锁机制内部原理深度解析

synchronized实现原理

synchronized基于JVM的内置锁实现,其内部机制涉及对象头、监视器锁等概念。

图2:synchronized锁升级机制图

ReentrantLock基于AQS的实现

ReentrantLock基于AbstractQueuedSynchronizer (AQS)实现,提供了更灵活的同步机制。

图3:AQS内部结构与等待队列示意图

// AQS核心方法简化实现示例
public abstract class AbstractQueuedSynchronizer {private volatile int state;private transient volatile Node head;private transient volatile Node tail;// 获取锁的核心逻辑public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}// 子类需要实现的模板方法protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}// ReentrantLock的具体实现static final class NonfairSync extends Sync {protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 尝试CAS设置状态if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 可重入逻辑int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
}

性能对比分析

不同锁机制特性对比

锁类型

可重入性

公平性

超时机制

中断响应

性能

使用复杂度

Mutex

中等

synchronized

简单

ReentrantLock

中等

复杂

ReadWriteLock

读高写低

复杂

性能基准测试

import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;public class LockPerformanceTest {private static final int THREAD_COUNT = 10;private static final int OPERATIONS_PER_THREAD = 100000;private int synchronizedCounter = 0;private int reentrantLockCounter = 0;private final ReentrantLock lock = new ReentrantLock();// synchronized性能测试public synchronized void synchronizedIncrement() {synchronizedCounter++;}// ReentrantLock性能测试public void reentrantLockIncrement() {lock.lock();try {reentrantLockCounter++;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {LockPerformanceTest test = new LockPerformanceTest();// 测试synchronizedlong syncStartTime = System.nanoTime();test.testSynchronized();long syncEndTime = System.nanoTime();// 测试ReentrantLocklong lockStartTime = System.nanoTime();test.testReentrantLock();long lockEndTime = System.nanoTime();System.out.printf("Synchronized耗时: %.2f ms\n", (syncEndTime - syncStartTime) / 1_000_000.0);System.out.printf("ReentrantLock耗时: %.2f ms\n", (lockEndTime - lockStartTime) / 1_000_000.0);System.out.println("Synchronized计数器: " + test.synchronizedCounter);System.out.println("ReentrantLock计数器: " + test.reentrantLockCounter);}private void testSynchronized() throws InterruptedException {CountDownLatch latch = new CountDownLatch(THREAD_COUNT);for (int i = 0; i < THREAD_COUNT; i++) {new Thread(() -> {for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {synchronizedIncrement();}latch.countDown();}).start();}latch.await();}private void testReentrantLock() throws InterruptedException {CountDownLatch latch = new CountDownLatch(THREAD_COUNT);for (int i = 0; i < THREAD_COUNT; i++) {new Thread(() -> {for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {reentrantLockIncrement();}latch.countDown();}).start();}latch.await();}
}

性能测试结果分析

图4:不同并发度下各种锁机制的性能对比图

死锁问题与解决方案

死锁产生的四个必要条件

  1. 互斥条件:资源不能被多个线程同时使用
  2. 持有并等待:线程持有资源的同时等待其他资源
  3. 不可剥夺:资源不能被强制从线程中剥夺
  4. 循环等待:存在线程资源的循环等待链

死锁场景代码复现

public class DeadlockExample {private static final Object lock1 = new Object();private static final Object lock2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock1) {System.out.println("Thread1: 获取到lock1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread1: 尝试获取lock2");synchronized (lock2) {System.out.println("Thread1: 获取到lock2");}}});Thread thread2 = new Thread(() -> {synchronized (lock2) {System.out.println("Thread2: 获取到lock2");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread2: 尝试获取lock1");synchronized (lock1) {System.out.println("Thread2: 获取到lock1");}}});thread1.start();thread2.start();}
}

死锁形成过程时序图

图5:死锁形成过程时序图

死锁解决方案

1. 顺序锁定法
public class OrderedLockSolution {private static final Object lock1 = new Object();private static final Object lock2 = new Object();// 定义锁的顺序private static void acquireLocksInOrder(Object firstLock, Object secondLock, Runnable task) {synchronized (firstLock) {synchronized (secondLock) {task.run();}}}public static void main(String[] args) {Thread thread1 = new Thread(() -> {System.out.println("Thread1: 开始执行");acquireLocksInOrder(lock1, lock2, () -> {System.out.println("Thread1: 获取到所有锁,执行业务逻辑");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println("Thread1: 执行完成");});Thread thread2 = new Thread(() -> {System.out.println("Thread2: 开始执行");acquireLocksInOrder(lock1, lock2, () -> {System.out.println("Thread2: 获取到所有锁,执行业务逻辑");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println("Thread2: 执行完成");});thread1.start();thread2.start();}
}
2. 超时锁定法
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;public class TimeoutLockSolution {private static final ReentrantLock lock1 = new ReentrantLock();private static final ReentrantLock lock2 = new ReentrantLock();private static boolean acquireLocksWithTimeout(ReentrantLock firstLock, ReentrantLock secondLock, long timeout, TimeUnit unit) {try {if (firstLock.tryLock(timeout, unit)) {try {if (secondLock.tryLock(timeout, unit)) {return true;} else {System.out.println("无法获取第二个锁,避免死锁");return false;}} finally {if (secondLock.isHeldByCurrentThread()) {secondLock.unlock();}}} else {System.out.println("无法获取第一个锁,避免死锁");return false;}} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;} finally {if (firstLock.isHeldByCurrentThread()) {firstLock.unlock();}}}public static void main(String[] args) {Thread thread1 = new Thread(() -> {System.out.println("Thread1: 开始尝试获取锁");if (acquireLocksWithTimeout(lock1, lock2, 2, TimeUnit.SECONDS)) {try {System.out.println("Thread1: 获取到所有锁,执行业务逻辑");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock2.unlock();lock1.unlock();}} else {System.out.println("Thread1: 获取锁失败,执行备用逻辑");}});Thread thread2 = new Thread(() -> {System.out.println("Thread2: 开始尝试获取锁");if (acquireLocksWithTimeout(lock2, lock1, 2, TimeUnit.SECONDS)) {try {System.out.println("Thread2: 获取到所有锁,执行业务逻辑");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock1.unlock();lock2.unlock();}} else {System.out.println("Thread2: 获取锁失败,执行备用逻辑");}});thread1.start();thread2.start();}
}

实际应用场景

1. 生产者-消费者模式

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
import java.util.Queue;public class ProducerConsumerExample {private final Queue<Integer> buffer = new LinkedList<>();private final int capacity = 10;private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();public void produce(int item) throws InterruptedException {lock.lock();try {while (buffer.size() == capacity) {System.out.println("缓冲区已满,生产者等待");notFull.await();}buffer.offer(item);System.out.println("生产者生产: " + item + ", 当前缓冲区大小: " + buffer.size());notEmpty.signalAll();} finally {lock.unlock();}}public Integer consume() throws InterruptedException {lock.lock();try {while (buffer.isEmpty()) {System.out.println("缓冲区为空,消费者等待");notEmpty.await();}Integer item = buffer.poll();System.out.println("消费者消费: " + item + ", 当前缓冲区大小: " + buffer.size());notFull.signalAll();return item;} finally {lock.unlock();}}public static void main(String[] args) {ProducerConsumerExample example = new ProducerConsumerExample();// 创建生产者线程Thread producer = new Thread(() -> {try {for (int i = 1; i <= 15; i++) {example.produce(i);Thread.sleep(100);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}});// 创建消费者线程Thread consumer = new Thread(() -> {try {for (int i = 1; i <= 15; i++) {example.consume();Thread.sleep(150);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}});producer.start();consumer.start();}
}

2. 读写锁在缓存系统中的应用

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.HashMap;
import java.util.Map;public class CacheWithReadWriteLock<K, V> {private final Map<K, V> cache = new HashMap<>();private final ReadWriteLock lock = new ReentrantReadWriteLock();public V get(K key) {lock.readLock().lock();try {System.out.println("读取缓存: " + key);return cache.get(key);} finally {lock.readLock().unlock();}}public void put(K key, V value) {lock.writeLock().lock();try {System.out.println("写入缓存: " + key + " = " + value);cache.put(key, value);} finally {lock.writeLock().unlock();}}public void remove(K key) {lock.writeLock().lock();try {System.out.println("删除缓存: " + key);cache.remove(key);} finally {lock.writeLock().unlock();}}public int size() {lock.readLock().lock();try {return cache.size();} finally {lock.readLock().unlock();}}public static void main(String[] args) throws InterruptedException {CacheWithReadWriteLock<String, String> cache = new CacheWithReadWriteLock<>();// 写入数据Thread writer = new Thread(() -> {for (int i = 0; i < 5; i++) {cache.put("key" + i, "value" + i);try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});// 读取数据Thread reader1 = new Thread(() -> {for (int i = 0; i < 10; i++) {cache.get("key" + (i % 5));try {Thread.sleep(50);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});Thread reader2 = new Thread(() -> {for (int i = 0; i < 10; i++) {cache.get("key" + (i % 5));try {Thread.sleep(60);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});writer.start();reader1.start();reader2.start();writer.join();reader1.join();reader2.join();System.out.println("最终缓存大小: " + cache.size());}
}

最佳实践与性能调优

锁使用最佳实践

  1. 最小化锁的粒度:只在必要的代码段使用锁
  2. 避免在锁内调用未知方法:防止意外的阻塞或死锁
  3. 使用try-finally确保锁释放:防止锁泄漏
  4. 考虑使用无锁数据结构:如ConcurrentHashMap、AtomicInteger等
// 良好的锁使用示例
public class BestPracticeExample {private final ReentrantLock lock = new ReentrantLock();private int counter = 0;// ✅ 好的做法:最小化锁粒度public void goodPractice() {// 在锁外进行耗时操作String result = expensiveComputation();lock.lock();try {// 只在必要时持有锁counter++;processResult(result);} finally {lock.unlock();}}// ❌ 坏的做法:锁粒度过大public void badPractice() {lock.lock();try {// 在锁内进行耗时操作,降低并发性String result = expensiveComputation();counter++;processResult(result);} finally {lock.unlock();}}private String expensiveComputation() {// 模拟耗时操作try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}return "computed result";}private void processResult(String result) {// 处理结果System.out.println("Processing: " + result);}
}

性能调优策略

"过早的优化是万恶之源" - Donald Knuth

但在高并发场景下,合理的锁优化是必要的:

  1. 锁消除(Lock Elimination):JIT编译器会消除不可能存在竞争的锁
  2. 锁粗化(Lock Coarsening):将多个连续的锁操作合并为一个
  3. 偏向锁优化:针对大多数情况下锁只被一个线程访问的场景
  4. 轻量级锁:使用CAS操作替代重量级锁
// JVM锁优化示例
public class LockOptimizationExample {private int counter = 0;// 锁消除:JIT会发现这里不存在竞争public void lockElimination() {Object localLock = new Object();synchronized (localLock) {counter++;}}// 锁粗化:多个连续的同步块会被合并public void lockCoarsening() {synchronized (this) {counter++;}synchronized (this) {counter++;}synchronized (this) {counter++;}// JIT优化后等效于:// synchronized (this) {//     counter++;//     counter++;//     counter++;// }}
}

总结与展望

回顾这篇文章的写作过程,我深深感受到并发编程领域的博大精深。从最初的Mutex互斥锁到现代的ReentrantLock,每一个演进都体现了计算机科学家们对性能和可用性平衡的不断追求。在我十多年的开发生涯中,我见证了从单核时代的简单同步到多核时代的复杂并发控制,每一次技术的迭代都带来了新的挑战和机遇。

特别值得一提的是,现代并发编程已经不再是简单的锁使用,而是需要开发者具备系统性的思维。我们需要理解CPU缓存一致性、内存模型、以及各种同步原语的适用场景。在实际项目中,我经常看到开发者因为对锁机制理解不深而导致的性能问题。比如,过度使用synchronized导致的锁竞争、不合理的锁粒度设计引发的死锁、以及忽视读写比例而错误选择锁类型等。

从技术发展趋势来看,未来的并发编程将朝着更加智能化和自动化的方向发展。无锁编程、软件事务内存(STM)、Actor模型等新兴技术正在逐步成熟,它们为解决传统锁机制的局限性提供了新的思路。同时,随着硬件技术的发展,新的CPU指令集和内存模型也在不断推动并发编程理论和实践的进步。

对于正在学习并发编程的读者,我有几点建议:首先,要打好理论基础,深入理解内存模型、原子性、可见性等核心概念;其次,要多动手实践,通过编写和调试多线程程序来加深理解;最后,要保持学习的热情,关注新技术的发展动态。并发编程是一个需要不断学习和实践的领域,只有通过持续的努力,才能真正掌握这门艺术。

记住,编写正确的并发程序比编写高性能的并发程序更重要。在追求性能的同时,永远不要忽视程序的正确性和可维护性。愿每一位开发者都能在并发编程的道路上走得更远,创造出更加优秀的软件作品。


参考资料:

  • Java并发编程实战 - Oracle官方文档
  • 深入理解Java虚拟机 - Java语言规范
  • The Art of Multiprocessor Programming - OpenJDK源码

"并发编程就像是在钢丝上跳舞,需要极致的平衡感和丰富的经验。" - 《Java并发编程实战》

🌟 嗨,我是IRpickstars!如果你觉得这篇技术分享对你有启发:

🛠️ 点击【点赞】让更多开发者看到这篇干货
🔔 【关注】解锁更多架构设计&性能优化秘籍
💡 【评论】留下你的技术见解或实战困惑

作为常年奋战在一线的技术博主,我特别期待与你进行深度技术对话。每一个问题都是新的思考维度,每一次讨论都能碰撞出创新的火花。

🌟 点击这里👉 IRpickstars的主页 ,获取最新技术解析与实战干货!

⚡️ 我的更新节奏:

  • 每周三晚8点:深度技术长文
  • 每周日早10点:高效开发技巧
  • 突发技术热点:48小时内专题解析

 

http://www.dtcms.com/a/273752.html

相关文章:

  • 大模型使用
  • Qt 实现新手引导
  • Windows解决 ping 127.0.0.1 一般故障问题
  • unity 有打击感的图片,怎么做动画,可以表现出良好的打击效果
  • STM32串口通信(寄存器与hal库实现)
  • 2025年7月11日学习笔记一周归纳——模式识别与机器学习
  • 高校智慧教室物联网系统设计与实现
  • 《磁力下载工具实测:资源搜索+高速下载一站式解决方案》
  • 串行数据检测器,检测到011,Y输出1,否则为0.
  • JavaScript加强篇——第五章 DOM节点(加强)与BOM
  • 网安系列【18】之SpringBoot漏洞
  • React Three Fiber 实现 3D 模型点击高亮交互的核心技巧
  • 小架构step系列11:单元测试引入
  • Rocky Linux上使用NVM安装Node.js 18
  • 老系统改造增加初始化,自动化数据源配置(tomcat+jsp+springmvc)
  • 大数据时代UI前端的用户体验设计新思维:以数据为驱动的情感化设计
  • golang -gorm 增删改查操作,事务操作
  • 分布式推客系统全栈开发指南:SpringCloud+Neo4j+Redis实战解析
  • Neo4j启动
  • 从一到无穷大 #47:浅谈对象存储加速
  • 基于vscode的go环境安装简介
  • 企业级LLM知识库:构建智能知识管理平台,赋能业务增长
  • 降本增效!上云真香!
  • 如何批量旋转视频90度?
  • 基于Selenium和FFmpeg的全平台短视频自动化发布系统
  • 通过命名空间引用了 Application 类,php不会自动包含路径文件吗?
  • Vue 中的属性绑定:从基础到实战进阶
  • docker0网卡没有ip一步解决
  • Kotlin基础
  • leetcode 3169. 无需开会的工作日 中等