翻译自geeksforgeeks。
信号量Semaphore
通过使用计数器counter
来控制对共享资源的访问。如果计数器大于零,则允许访问。如果为零,则拒绝访问。计数器对共享资源的访问许可进行计数。因此,要访问资源,线程必须要从信号量得到许可。
信号量如何工作
一般而言,当一个线程需要访问共享资源时,需要获得许可,这里就要用到信号量Semaphore
。
- 如果信号量的计数
count
大于零,则线程获取许可permit
,导致信号量的计数递减。 - 否则,线程将被阻塞,直到获得许可
permit
。 - 当线程不再需要访问共享资源时,它会释放许可
permit
,这会导致信号量的计数count递增。 - 如果有另一个线程在等待许可,那么该线程将在那时获得许可。
Java在java.util.concurrent包中提供了实现此机制的Semaphore类,因此你不必实现自己的信号量。
流程图:
Semaphore 的构造函数有两种:
Semaphore(int num)
Semaphore(int num, boolean how)
这里,num
指定初始的许可计数。因此,它也就指定了一次可以同时访问共享资源的线程数。如果是1,那么同时只能有一个线程可以访问该资源。
默认情况下,所有等待的线程都以未定义的顺序被授予许可。通过设置how
为true,可以确保等待线程按其请求访问的顺序被授予许可。
使用信号量作为锁(防止竞争条件)
我们可以使用信号量来锁定对资源的访问,每个想要使用该资源的线程在访问资源之前必须首先调用acquire()
方法来获取锁。当线程不再需要资源时,它必须调用release()
方法来释放锁。这里有个例子来说明:
// java program to demonstrate
// use of semaphores Locks
import java.util.concurrent.*;
//A shared resource/class.
class Shared
{
static int count = 0;
}
class MyThread extends Thread
{
Semaphore sem;
String threadName;
public MyThread(Semaphore sem, String threadName)
{
super(threadName);
this.sem = sem;
this.threadName = threadName;
}
@Override
public void run() {
// run by thread A
if(this.getName().equals("A"))
{
System.out.println("Starting " + threadName);
try
{
// 首先获取许可
System.out.println(threadName + " is waiting for a permit.");
// 获取锁
sem.acquire();
System.out.println(threadName + " gets a permit.");
// 访问共享资源
// 其他等待线程会一直等到次线程释放锁
for(int i=0; i < 5; i++)
{
Shared.count++;
System.out.println(threadName + ": " + Shared.count);
// 允许上下文切换
// 让线程B运行
Thread.sleep(10);
}
} catch (InterruptedException exc) {
System.out.println(exc);
}
// 释放许可
System.out.println(threadName + " releases the permit.");
sem.release();
}
// run by thread B
else
{
System.out.println("Starting " + threadName);
try
{
// First, get a permit.
System.out.println(threadName + " is waiting for a permit.");
// acquiring the lock
sem.acquire();
System.out.println(threadName + " gets a permit.");
// Now, accessing the shared resource.
// other waiting threads will wait, until this
// thread release the lock
for(int i=0; i < 5; i++)
{
Shared.count--;
System.out.println(threadName + ": " + Shared.count);
// Now, allowing a context switch -- if possible.
// for thread A to execute
Thread.sleep(10);
}
} catch (InterruptedException exc) {
System.out.println(exc);
}
// Release the permit.
System.out.println(threadName + " releases the permit.");
sem.release();
}
}
}
// Driver class
public class SemaphoreDemo
{
public static void main(String args[]) throws InterruptedException
{
// 创建一个Semaphore对象
// 许可数设置为1
Semaphore sem = new Semaphore(1);
// 创建名为A和B的两个线程
// 注意,线程A将递增count
// 线程B将递减count
MyThread mt1 = new MyThread(sem, "A");
MyThread mt2 = new MyThread(sem, "B");
// 启动线程A和B.
mt1.start();
mt2.start();
// 等待线程A和B.
mt1.join();
mt2.join();
//两个线程都完成执行后count会始终为0
System.out.println("count: " + Shared.count);
}
}
输出:
Starting A
A is waiting for a permit.
A gets a permit.
A: 1
Starting B
B is waiting for a permit.
A: 2
A: 3
A: 4
A: 5
A releases the permit.
B gets a permit.
B: 4
B: 3
B: 2
B: 1
B: 0
B releases the permit.
count: 0
注意:在上述程序的不同执行中输出可能不同,但count变量的最终值将始终保持为0。
解释:
- 程序使用信号量来控制对
count
变量的访问,count
变量是Shared
类中的静态变量。Shared.count
由线程A递增五次,并由线程B递减五次。为了防止这两个线程同时访问Shared.count,只有在从控制信号量获取许可后才允许访问。访问完成后,许可将被释放。这样,一次只有一个线程将访问Shared.count,如输出所示。 - 注意在
MyThread
类中的run()
方法中调用sleep()
方法,用于“证明”对Shared.count
的访问被信号量同步了。在run()
方法中,对sleep()
的调用会导致线程在每次访问Shared.count
之间暂停。这通常会使另一个线程运行。然而,由于信号量,第二个线程必须等到第一个线程释放许可,这只有在第一个线程的所有访问完成后才会发生。因此,Shared.count
首先由线程A递增五次,然后由线程B递减五次。递增和递减不会在汇编代码中交叉执行。 - 如果不使用信号量,两个线程对
Shared.count
的访问将同时发生,递增和递减会交叉执行。要确认这一点,可以尝试注释acquire()
和release()
。运行程序时,会看到对Shared.count的访问不再同步,因此不会始终获得计数值为0。