16. Semaphore: How to quickly implement a current limiter? -Concurrency tools


Semaphore, translated as "semaphore". First introduce the semaphore model, then introduce how to use the semaphore, and finally we use the semaphore to implement a current limiter.

Semaphore model

Insert picture description here
A counter, a waiting queue, and three methods. The counter and the waiting queue are opaque and can only be accessed through three methods.

  • init (), set the initial value of the counter.
  • down (): The value of the counter is decremented by 1; if the value of the counter is less than 0 at this time, the current thread will be blocked, otherwise the current thread can continue to execute.
  • up (): increase the value of the counter by 1; if the value of the counter is less than or equal to 0 at this time, wake up a thread in the waiting queue and remove it from the waiting queue.

The three methods init (), down () and up () are atomic and guaranteed by the implementor of the semaphore model.

class Semaphore {
	// 计数器
	int count;
	// 等待队列
	Queue queue;

	// 初始化操作
	Semaphore(int c) {
		this.count = c;
	}

	//
	void down() {
		this.count--;
		if (this.count < 0) {
			// 将当前线程插入等待队列
			// 阻塞当前线程
		}
	}

	void up() {
		this.count++;
		if (this.count <= 0) {
			// 移除等待队列中的某个线程 T
			// 唤醒线程 T
		}
	}
}

In the Java SDK concurrent package, down () and up () correspond to acquire () and release ().

2. How to use semaphores

The following code:

	static int count;
	// 初始化信号量
	static final Semaphore s = new Semaphore(1);

	// 用信号量保证互斥
	static void addOne() {
		s.acquire();
		try {
			count += 1;
		} finally {
			s.release();
		}
	}

The count + = 1 operation is a critical section, and only one thread is allowed to execute it. Perform a down () operation before entering the critical section and an up () operation before exiting the critical section. cquire () is the down () operation in the semaphore, and release () is the up () operation in the semaphore.

Suppose that two threads T1 and T2 access the addOne () method at the same time. When they call acquire () at the same time, since acquire () is an atomic operation, only one thread (assuming T1) decrements the counter in the semaphore Is 0, the other thread (T2) is to reduce the counter to -1. For thread T1, the counter value in the semaphore is 0, greater than or equal to 0, so thread T1 will continue to execute; for thread T2, the counter value in the semaphore is -1, less than 0, according to the semaphore model for down () Description of operation, thread T2 will be blocked. So at this time, only thread T1 will enter the critical section and execute count + = 1.

When the thread T1 executes the release () operation, that is, the up () operation, the value of the counter in the semaphore is -1, and the value after adding 1 is 0, which is less than or equal to 0. According to the semaphore model, the up () operation Description, at this time T2 in the waiting queue will be woken up. So T2 only got the opportunity to enter the critical section for execution after T1 executed the critical section code, thus ensuring mutual exclusion.

3. Quickly implement a current limiter

There is another function of Semaphore that Lock is not easy to implement, that is: Semaphore can allow multiple threads to access a critical section .

The more common requirements in reality are the various pooled resources we encounter in our work, such as connection pools, object pools, thread pools, and so on. For example, the database connection pool must allow multiple threads to use the connection pool at the same time. Of course, each connection is not allowed to be used by other threads before it is released.

Current limiter refers to not allowing more than N threads to enter the critical section at the same time.

The semaphore counter is set to 1, indicating that only one thread is allowed to enter the critical section, and is set to the number N of objects in the object pool, indicating that N threads are allowed to enter the critical section.

class ObjPool<T, R> {
	final List<T> pool;
	// 用信号量实现限流器
	final Semaphore sem;

	// 构造函数
	ObjPool(int size, T t) {
		pool = new Vector<T>() {
		};
		for (int i = 0; i < size; i++) {
			pool.add(t);
		}
		sem = new Semaphore(size);
	}

	// 利用对象池的对象,调用 func
	R exec(Function<T, R> func) {
		T t = null;
		sem.acquire();
		try {
			t = pool.remove(0);
			return func.apply(t);
		} finally {
			pool.add(t);
			sem.release();
		}
	}
}
	// 创建对象池
	ObjPool<Long, String> pool = new ObjPool<Long, String>(10, 2);
	// 通过对象池获取 t,之后执行
	pool.exec(t->
	{
	    System.out.println(t);
	    return t.toString();
	});
  • We use a List to store object instances, and use Semaphore to implement a current limiter.
  • The key code is the exec () method in ObjPool, which implements the current limit function.
  • In this method, we first call the acquire () method (the matching one is to call the release () method in finally),
  • Assuming that the size of the object pool is 10, and the semaphore counter is initialized to 10, then the first 10 threads calling the acquire () method can continue to execute, which is equivalent to passing the semaphore, while other threads will block on the acquire () method .
  • For the thread passing through the semaphore, we allocated an object t for each thread (this allocation is achieved through pool.remove (0)). After the allocation, a callback function func is executed, and the parameters of the function are the previous allocation The object t;
  • After the callback function is executed, they will release the object (this release is achieved by pool.add (t)), and call the release () method to update the semaphore counter.
  • If the value of the counter in the semaphore is less than or equal to 0 at this time, it means that there is a thread waiting, and the waiting thread will be automatically awakened at this time.
Published 97 original articles · praised 3 · 10,000+ views

Guess you like

Origin blog.csdn.net/qq_39530821/article/details/102539511