在现代计算机系统中,多线程编程已经成为一种必不可少的技能。然而,多线程编程中最棘手的问题之一是如何控制线程之间的并发访问。在多线程编程中,多个线程同时访问共享资源时,很容易出现数据竞争、死锁、饥饿等问题,这些问题会导致程序的不稳定和不可预测性。
Semaphore 是一种并发编程中常用的同步机制,它提供了一种简单而有效的方法来控制多个线程之间的并发访问。Semaphore 可以用于限制同时访问某个共享资源的线程数量,或者在多个线程之间协调执行顺序。
在本文中,我们将深入探讨 Semaphore 的概念和原理,介绍 Semaphore 在多线程编程中的应用场景以及如何使用 Semaphore 来解决并发编程中的常见问题。我们还将探讨 Semaphore 在不同编程语言和操作系统中的实现,并介绍一些最佳实践和经验教训,以帮助读者更好地理解和应用 Semaphore。
一、Semaphore是什么?
Semaphore是计数信号量。Semaphore管理一系列许可。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可这个对象,Semaphore只是维持了一个可获得许可证的数量。
比如:停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。
比如:在学生时代都去餐厅打过饭,假如有3个窗口可以打饭,同一时刻也只能有3名同学打饭。第四个人来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了。
二、怎么使用Semaphore
2.1构造方法
Semaphore(int permits):这个构造器会创建一个Semaphore对象,并初始化该对象的许可数量为permits。许可数量表示当前有多少个线程可以同时访问Semaphore控制的资源。如果许可数量为0,则所有尝试获取许可的线程都会被阻塞,直到有一个线程释放了一个许可。
Semaphore(int permits, boolean fair):这个构造器与上一个构造器相似,只是多了一个boolean类型的参数fair,表示Semaphore对象是否是公平的。如果fair为true,表示Semaphore对象是公平的,即等待时间最长的线程会优先获取许可;如果fair为false,表示Semaphore对象是非公平的,线程获取许可的顺序是不确定的。
//创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
//创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)
2.2重要方法
以下是Semaphore中常用的几个方法的介绍:
1.acquire(int permits):获取permits个许可,如果没有足够的许可,当前线程会被阻塞,直到有足够的许可。该方法会对许可数量进行减少。如果调用该方法时许可数量为0,则当前线程会一直被阻塞,直到有另一个线程释放了许可。
例如,如果使用以下语句从Semaphore对象semaphore中获取2个许可:
semaphore.acquire(2);
如果semaphore对象当前有5个许可,那么此时semaphore对象的许可数量将会减少2个。
2.release(int permits):释放permits个许可,使得其他被阻塞的线程可以获取许可。该方法会对许可数量进行增加。如果释放许可后,有被阻塞的线程可以获取许可,则会唤醒这些线程。
例如,如果使用以下语句向Semaphore对象semaphore中释放3个许可:
semaphore.release(3);
如果semaphore对象当前有2个许可,那么此时semaphore对象的许可数量将会增加3个。
3.tryAcquire(int permits, long timeout, TimeUnit unit):尝试获取permits个许可,在timeout时间内等待获取许可,如果在timeout时间内获取不到足够的许可,则返回false。该方法会对许可数量进行减少。如果调用该方法时许可数量为0,则会等待timeout时间,如果在timeout时间内有许可可用,则获取许可,返回true;否则返回false。
例如,如果使用以下语句尝试从Semaphore对象semaphore中获取2个许可,在等待1秒后如果还没有足够的许可则返回false:
boolean result = semaphore.tryAcquire(2, 1, TimeUnit.SECONDS);
如果semaphore对象当前有3个许可,则会成功获取2个许可,并返回true;如果在1秒内semaphore对象的许可数量不足2个,则会返回false。
4.availablePermits():获取Semaphore对象当前可用的许可数量。
int permits = semaphore.availablePermits();
则会返回semaphore对象当前可用的许可数量。
三、Semaphore使用案例和应用场景
3.1使用案例
这个案例就是模拟学生去餐厅打饭的案例。定义一个 Student 类,继承自 Thread 类。它有两个成员变量 sp 和 name,分别代表信号量和学生名字。它还有一个构造函数,用来初始化 sp 和 name 成员变量。
该类覆盖了 Thread 类的 run() 方法,实现了一个多线程的逻辑。在 run() 方法中,首先调用了 sp.acquire() 方法,该方法会使线程等待信号量,直到信号量的值为正数才能获取到许可证。当线程获得许可证时,输出该学生名字加上“拿到了打饭许可证”的信息。如果等待信号量的过程中被中断,则会抛出 InterruptedException 异常。无论是正常结束还是抛出异常,最终都会输出该学生名字加上“打好饭了,释放这个窗口”的信息,并且调用 sp.release() 方法,该方法会释放信号量,使得其他线程可以获得许可证。
通过使用信号量,这个代码模拟了多个学生在打饭时排队的场景,保证了只有有限数量的学生可以同时获取到打饭的许可证,从而避免了饭堂出现拥堵的情况。
public class Student extends Thread {
private Semaphore sp;
private String name;
public Student(Semaphore sp,String name){
this.sp=sp;
this.name=name;
}
@Override
public void run() {
try {
sp.acquire();
System.out.println(name+"拿到了打饭许可证");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(name+"打好饭了,释放这个窗口");
sp.release();
}
}
}
写一个测试类来测试下上面的Student学生打饭的场景
class TestSemaphore{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Student(semaphore,"学生"+i).start();
}
}
}
打印结果如下:
结果很清晰,所以对于Semaphore来说,我们需要记住的其实是资源的互斥而不是资源的同步,在同一时刻是无法保证同步的,但是却可以保证资源的互斥。
3.2应用场景
Semaphore主要用于那些资源有明确访问数量限制的场景,常用于限流 。
1、数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
2、停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。
3、实现生产者消费者模式Semaphore可以用于实现生产者消费者模式,控制生产者和消费者的并发数量,保证数据的安全和有效处理。
总结
Java中的Semaphore类是一种同步机制,用于控制同时访问共享资源的线程数量。Semaphore类提供了acquire()和release()方法来获取和释放信号量,控制线程的访问。Semaphore类可以应用于多种场景,例如控制线程池中的线程数量、限制数据库连接池中的连接数量等。Semaphore类还提供了一些高级功能,如公平性和超时机制等。在使用Semaphore类时,需要注意避免死锁和饥饿等问题。Semaphore类是线程安全的,并且具有高效性。
总之,Java Semaphore类是一种非常有用的同步机制,可以帮助我们控制共享资源的访问,提高程序的并发性和稳定性。在编写多线程应用程序时,可以充分利用Semaphore类提供的功能来实现同步和资源访问控制,并注意避免潜在的死锁和饥饿问题。