JDK源码详解——Semaphore,BlockingQueue,ArrayBlockingQueue
wptr33 2025-09-19 03:55 1 浏览
Semaphore
Semaphore 是并发包中的一个工具类,可理解为信号量。通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数。
打个通俗的比方,可以把 Semaphore 理解为一辆公交车:车上的座位数(初始的“许可” permits 数量)是固定的,行驶期间如果有人上车(获取许可),座位数(许可数量)就会减少,当人满的时候不能再继续上车了(获取许可失败);而有人下车(释放许可)后就空出了一些座位,其他人就可以继续上车了。
下面具体分析其代码实现。
代码分析
Semaphore 的方法如下:
其中主要方法是 acquire() 和 release() 相关的一系列方法,它们的作用类似。我们先从构造器开始分析。
构造器
private final Sync sync;
// 初始化 Semaphore,传入指定的许可数量,非公平
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 初始化 Semaphore,传入指定的许可数量,指定是否公平
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
构造器初始化了 Sync 变量,根据传入的 fair 值指定为 FairSync 或 NonFairSync,下面分析这三个类。
内部嵌套类 Sync:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
// 构造器,将父类 AQS 的 state 变量初始化为给定的 permits
Sync(int permits) {
setState(permits);
}
// 非公平方式尝试获取许可(减少 state 的值)
final int nonfairTryAcquireShared(int acquires) {
// 自旋操作
for (;;) {
// 获取许可值(state),并尝试 CAS 修改为减去后的结果
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 释放许可(增加 state 的值)
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 操作与获取类似,不同的在于此处是增加 state 值
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
// 一些方法未给出...
}
可以看到 Sync 类继承自 AQS,并重写了 AQS 的 tryReleaseShared 方法,其中获取和释放许可分别对应的是对 AQS 中 state 值的减法和加法操作。具体可参考前文对 AQS 共享模式的分析「万字长文详解!JDK源码
-AbstractQueuedSynchronizer(3)」。
NonFairSync (非公平版本实现):
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
// 调用父类 Sync 的构造器来实现
NonfairSync(int permits) {
super(permits);
}
// 重写 AQS 的 tryAcquireShared 方法,代码实现在父类 Sync 中
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
FairSync (公平版本实现):
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
// 构造器调用父类 Sync 的构造器来实现
FairSync(int permits) {
super(permits);
}
// 重写 AQS 的 tryAcquireShared 方法,尝试获取许可(permit)
protected int tryAcquireShared(int acquires) {
for (;;) {
// 若队列中有其他线程等待,则获取失败(这就是体现“公平”的地方)
if (hasQueuedPredecessors())
return -1;
// 获取当前的许可值
int available = getState();
// 计算剩余值
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
PS: 体现“公平”的地方在于 tryAcquireShared 方法中,公平的版本会先判断队列中是否有其它线程在等待(hasQueuedPredecessors 方法)。
主要方法的代码实现:
// 获取一个许可(可中断)
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 获取一个许可(不响应中断)
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
// 尝试获取一个许可
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
// 尝试获取一个许可(有超时等待)
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 释放一个许可
public void release() {
sync.releaseShared(1);
}
还有一系列类似的操作,只不过获取/释放许可的数量可以指定:
// 获取指定数量的许可(可中断)
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
// 获取指定数量的许可(不可中断)
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
// 尝试获取指定数量的许可
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
// 尝试获取指定数量的许可(有超时等待)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
// 释放指定数量的许可
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
可以看到,Semaphore 的主要方法都是在嵌套类 FairSync 和 NonFairSync 及其父类 Sync 中实现的,内部嵌套类也是 AQS 的典型用法。
场景举例
为了便于理解 Semaphore 的用法,下面简单举例分析(仅供参考):
public class SemaphoreTest {
public static void main(String[] args) {
// 初始化 Semaphore
// 这里的许可数为 2,即同时最多有 2 个线程可以获取到
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 50; i++) {
new Thread(() -> {
try {
// 获取许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 正在执行..");
// 模拟操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
}
}).start();
}
}
}
/* 执行结果(仅供参考):
Thread-0 正在执行..
Thread-1 正在执行..
Thread-2 正在执行..
Thread-3 正在执行..
...
*/
这里把 Semaphore 的初始许可值设为 2,表示最多有两个线程可同时获取到许可(运行程序可发现线程是两两一起执行的)。设置为其他值也是类似的。
比较特殊的是,如果把 Semaphore 的初始许可值设为 1,可以当做“互斥锁”来使用。
小结
Semaphore 是并发包中的一个工具类,其内部是基于 AQS 共享模式实现的。通常可以作为限流器使用,比如限定连接池等的大小。
BlockingQueue
BlockingQueue 意为“阻塞队列”,它在 JDK 中是一个接口。
所谓阻塞,简单来说就是当某些条件不满足时,让线程处于等待状态。例如经典的“生产者-消费者”模型,当存放产品的容器满的时候,生产者处于等待状态;而当容器为空的时候,消费者处于等待状态。阻塞队列的概念与该场景类似。
BlockingQueue 的继承关系如下:
可以看到 BlockingQueue 继承自 Queue 接口,它的常用实现类有 ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue 等。同前面一样,本文先分析 BlockingQueue 接口的方法定义,后文再分析其实现类的代码。
PS: 从这个继承体系也可以看出来,直接实现接口的是抽象类,而实现类则通常继承自抽象类。为什么要这样设计呢?因为有些接口的实现类会有多个,而这些类之间有一部分逻辑是相似或者相同的,因此就把这部分逻辑提取到抽象类中,避免代码冗余。
代码分析
BlockingQueue 的方法定义如下:
其方法简单分析如下:
// 将指定元素插入到队列,若成功返回 true,否则抛出异常
boolean add(E e);
// 将指定元素插入到队列,若成功返回 true,否则返回 false
boolean offer(E e);
// 将指定元素插入到队列,若队列已满则等待
void put(E e) throws InterruptedException;
// 将指定元素插入到队列,若成功返回 true,否则返回 false,有超时等待
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException
// 获取并移除队列的头部,若为空则等待
E take() throws InterruptedException;
// 获取并移除队列的头部,有超时等待(若超时返回 null)
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// 返回队列可以接收的容量,若无限制则返回 Integer.MAX_VALUE
int remainingCapacity();
// 删除指定的元素(如果存在),返回是否删除成功
boolean remove(Object o);
// 是否包含指定元素
public boolean contains(Object o);
// 从此队列中删除所有可用元素,并将它们添加到给定集合中
int drainTo(Collection<? super E> c);
// 从此队列中删除所有可用元素,并将它们添加到给定集合中(指定大小)
int drainTo(Collection<? super E> c, int maxElements);
主要方法小结如下:
Throws exceptions | Special value | Blocks | Time out | |
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | - | - |
Queue 接口前文「Queue, Deque」已进行分析,这里不再赘述。
典型用法
生产者:
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
// 将产品放入队列
while (true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
消费者:
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
// 从队列中消费产品
while (true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
测试类:
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
// 创建并启动一个生产者和两个消费者
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
PS: 上述代码是 BlockingQueue 的文档提供的,仅供参考。
小结
BlockingQueue 是一个接口,它主要定义了阻塞队列的一些方法。阻塞队列在并发编程中使用较多,比如线程池。
ArrayBlockingQueue
上面「BlockingQueue」简要分析了 BlockingQueue 接口的主要方法,ArrayBlockingQueue 就是该接口的一个主要实现类,本文分析该类的常用方法实现。
ArrayBlockingQueue 的类继承结构如下:
从 ArrayBlockingQueue 的名字大概可以猜出来,它的内部是由数组实现的,下面分析其代码实现。
代码分析
构造器
构造器 1:
// 构造器 1:初始化 ArrayBlockingQueue 对象,使用给定的容量
public ArrayBlockingQueue(int capacity) {
// 调用构造器 2 进行初始化,默认使用非公平锁
this(capacity, false);
}
构造器 2:
// 构造器 2:使用给定容量及是否公平初始化 ArrayBlockingQueue 对象
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 用给定的容量初始化内部数组
this.items = new Object[capacity];
// 创建锁对象(根据 fair 参数确定是否公平锁)
lock = new ReentrantLock(fair);
// lock 绑定两个 Condition 条件
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
构造器 3:
// 构造器 3:使用给定的容量、是否公平,及给定的集合初始化 ArrayBlockingQueue
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
// 使用构造器 2 初始化 ArrayBlockingQueue 对象
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
// 遍历给定集合的元素,将其插入数组
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
// 注意可能会发生数组越界
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
// 数组中元素的数量
count = i;
// 入队操作(put、offer 等方法)的数组下标,若数组已满则为 0
putIndex = (i == capacity) ? 0 : i;
} finally {
// 注意释放锁
lock.unlock();
}
}
主要成员变量
/** The queued items */
// 内部保存元素的数组
final Object[] items;
/** items index for next take, poll, peek or remove */
// 出队操作索引
int takeIndex;
/** items index for next put, offer, or add */
// 入队操作索引
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
* 双条件(notEmpty、notFull)算法用于并发控制
*/
/** Main lock guarding all access */
// 使用 ReentrantLock 保证线程安全
final ReentrantLock lock;
/** Condition for waiting takes */
// 等待 take 操作(消费)的条件
private final Condition notEmpty;
/** Condition for waiting puts */
// 等待 put 操作(生产)的条件
private final Condition notFull;
主要入队方法:add(E), offer(E), offer(E, timeout, Unit), put(E)
1. add(E) 方法
public boolean add(E e) {
// 调用父类 AbstractQueue 的 add 方法
return super.add(e);
}
// AbstractQueue 的 add 方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
add(E) 方法调用了父类 AbstractQueue 的 add(E) 方法,可以看到,实际上还是调用了 offer(E) 方法。因此 add(E) 和 offer(E) 实现基本是一致的,下面分析 offer(E) 方法。
2. offer(E), offer(E, timeout, Unit) 方法
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 若队列已满,立即返回 false
if (count == items.length)
return false;
else {
// 入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
enqueue 方法:
// 入队操作
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
// 若队列已满,则下标置为 0
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒 notEmpty 条件下等待的线程
notEmpty.signal();
}
offer(E) 方法是将一个元素入队:若队列已满直接返回 false,否则执行入队操作,并唤醒 notEmpty 条件下等待的线程。
以“生产者-消费者”模型类比,执行 offer(E) 操作后表示队列已经有产品了(不为空,即 notEmpty),消费者可以消费了。
offer(E, timeout, Unit) 方法操作与 offer(E) 类似,只是多了超时等待,如下:
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
3. put(E) 方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列满的时候,notFull 条件等待
while (count == items.length)
notFull.await();
// 入队
enqueue(e);
} finally {
lock.unlock();
}
}
put(E) 也是将一个元素入队:若队列已满,则 notFull 条件下的线程等待。
以“生产者-消费者”模型类比,就是容器已满,生产者等待;否则执行入队,并唤醒消费者。
入队方法小结
1. add(E): 入队成功返回 true,否则抛出 IllegalStateException 异常;
2. offer(E): 入队成功返回 true,失败返回 false;
3. offer(E, timeout, Unit): 同 offer(E),加了超时等待;
4. put(E): 无返回值,队列满的时候等待。
主要出队方法:poll(), poll(long, unit), take(), peek()
1. poll() 方法
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 队列为空时返回 null,否则将 takeIndex 位置元素出队
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
2. take(E) 方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列为空时等待
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
3. poll(E, unit) 方法
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
与 poll() 方法操作类似,只是多了超时等待。
上述三个方法都使用 dequeue 方法进行出队,如下:
// 出队操作
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 获取 takeIndex 位置的元素
E x = (E) items[takeIndex];
// 将 该位置清空
items[takeIndex] = null;
// 队列已经空了
if (++takeIndex == items.length)
takeIndex = 0;
count--;
// 迭代器操作用到,本文暂不深入分析
if (itrs != null)
itrs.elementDequeued();
// 队列已经不满(not full)了,可以继续生产
notFull.signal();
return x;
}
4. peek() 方法
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
// 返回数组中指定位置的元素
final E itemAt(int i) {
return (E) items[i];
}
peek() 方法与前面几个出队操作不同,peek 方法只会获取队列的头元素,而不会将其删除。
出队方法小结
1. poll(): 获取队列头部元素,并将其移除,队列为空时返回 null;
2. take(): 获取队列头部元素,并将其移除,队列为空时阻塞等待;
3. poll(long, unit): 获取队列头部元素,并将其移除,队列为空时等待一段时间,若超时返回 null;
4. peek(): 获取队列头部元素,但不移除该元素。
小结
1. ArrayBlockingQueue 是基于数组的阻塞队列实现,它在初始化时需要指定容量;
2. 内部使用了 ReentrantLock 保证线程安全;
3. 常用方法:
入队:add, offer, put
出队:poll, take, peek
本文分析了其常用的方法,此外,还有一些方法使用频率没那么高且稍微复杂,例如 iterator() 和 drainTo(),后文再进行分析。
相关推荐
- 高性能并发队列Disruptor使用详解
-
基本概念Disruptor是一个高性能的异步处理框架,是一个轻量的Java消息服务JMS,能够在无锁的情况下实现队列的并发操作Disruptor使用环形数组实现了类似队列的功能,并且是一个有界队列....
- Disruptor一个高性能队列_java高性能队列
-
Disruptor一个高性能队列前言说到队列比较熟悉的可能是ArrayBlockingQueue、LinkedBlockingQueue这两个有界队列,大多应用在线程池中使用能保证线程安全,但其安全性...
- 谈谈防御性编程_防御性策略
-
防御性编程对于程序员来说是一种良好的代码习惯,是为了保护自己的程序在不可未知的异常下,避免带来更大的破坏性崩溃,使得程序在错误发生时,依然能够云淡风轻的处理,但很多程序员入行很多年,写出的代码依然都是...
- 有人敲门,开水开了,电话响了,孩子哭了,你先顾谁?
-
前言哎呀,这种情况你肯定遇到过吧!正在家里忙活着,突然——咚咚咚有人敲门,咕噜咕噜开水开了,铃铃铃电话响了,哇哇哇孩子又哭了...我去,四件事一起来,人都懵了!你说先搞哪个?其实这跟我们写Java多线...
- 面试官:线程池如何按照core、max、queue的执行顺序去执行?
-
前言这是一个真实的面试题。前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core、max、queue的执行循序去执行?"。我们都知道线程池中代码执行顺序是:co...
- 深入剖析 Java 中线程池的多种实现方式
-
在当今高度并发的互联网软件开发领域,高效地管理和利用线程资源是提升程序性能的关键。Java作为一种广泛应用于后端开发的编程语言,为我们提供了丰富的线程池实现方式。今天,就让我们深入探讨Java中...
- 并发编程之《彻底搞懂Java线程》_java多线程并发解决方案详解
-
目录引言一、核心概念:线程是什么?...
- Redis怎么实现延时消息_redis实现延时任务
-
一句话总结Redis可通过有序集合(ZSET)实现延时消息:将消息作为value,到期时间戳作为score存入ZSET。消费者轮询用ZRANGEBYSCORE获取到期消息,配合Lua脚本保证原子性获取...
- CompletableFuture真的用对了吗?盘点它最容易被误用的5个场景
-
在Java并发编程中,CompletableFuture是处理异步任务的利器,但不少开发者在使用时踩过这些坑——线上服务突然雪崩、异常悄无声息消失、接口响应时间翻倍……本文结合真实案例,拆解5个最容易...
- 接口性能优化技巧,有点硬_接口性能瓶颈
-
背景我负责的系统到2021年初完成了功能上的建设,开始进入到推广阶段。随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽。刚刚收到吐槽的时候,我们的心情是这样的:...
- 禁止使用这5个Java类,每一个背后都有一段"血泪史"
-
某电商平台的支付系统突然报警:大量订单状态异常。排查日志发现,同一笔订单被重复支付了三次。事后复盘显示,罪魁祸首竟是一行看似无害的SimpleDateFormat代码。在Java开发中,这类因使用不安...
- 无锁队列Disruptor原理解析_无锁队列实现原理
-
队列比较队列...
- Java并发队列与容器_java 并发队列
-
【前言:无论是大数据从业人员还是Java从业人员,掌握Java高并发和多线程是必备技能之一。本文主要阐述Java并发包下的阻塞队列和并发容器,其实研读过大数据相关技术如Spark、Storm等源码的,...
- 线程池工具及拒绝策略的使用_线程池处理策略
-
线程池的拒绝策略若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。...
- 【面试题精讲】ArrayBlockingQueue 和 LinkedBlockingQueue 区别?
-
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
如何将AI助手接入微信(打开ai手机助手)
-
Java面试必考问题:什么是乐观锁与悲观锁
-
SparkSQL——DataFrame的创建与使用
-
redission YYDS spring boot redission 使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)