并发编程中的同步工具与联合动作
立即解锁
发布时间: 2025-08-17 02:17:23 阅读量: 3 订阅数: 4 

### 并发编程中的同步工具与联合动作
#### 1. 同步通道
同步通道(Synchronous channels)是一种特殊的通道,其接口可以描述为支持 `put` 和 `take` 操作的通用通道。以下是通道接口的定义:
```java
interface Channel {
void put(Object x) throws InterruptedException;
Object take() throws InterruptedException;
}
```
同步通道没有内部容量,每个尝试 `put` 的线程必须等待一个尝试 `take` 的线程,反之亦然。可以使用信号量(Semaphores)来实现同步通道,示例代码如下:
```java
class SynchronousChannel implements Channel {
protected Object item = null;
protected final Semaphore putPermit;
protected final Semaphore takePermit;
protected final Semaphore taken;
public SynchronousChannel() {
putPermit = new Semaphore(1);
takePermit = new Semaphore(0);
taken = new Semaphore(0);
}
public void put(Object x) throws InterruptedException {
putPermit.acquire();
item = x;
takePermit.release();
InterruptedException caught = null;
for (;;) {
try {
taken.acquire();
break;
} catch(InterruptedException ie) { caught = ie; }
}
if (caught != null) throw caught;
}
public Object take() throws InterruptedException {
takePermit.acquire();
Object x = item;
item = null;
putPermit.release();
taken.release();
return x;
}
}
```
这个实现中,`put` 方法在释放 `takePermit` 后,需要等待 `taken` 信号量,以确保放入的元素被取走。
#### 2. 公平性和调度
内置的等待和通知方法不提供公平性保证。在 `notify` 操作中,无法确定等待集中的哪个线程会被选中;在 `notifyAll` 操作中,也无法确定哪个线程会先获得锁并继续执行。
虽然无法改变 `notify` 的语义,但可以实现信号量的 `acquire` 操作来提供更强的公平性属性。常见的公平性策略是先进先出(FIFO),即等待时间最长的线程总是被优先选择。不过,在多处理器环境中,FIFO 策略可能过于严格。可以使用一些弱化或近似 FIFO 的策略,以满足避免无限延迟的应用需求。
例如,`FIFOSemaphore` 类可以维护 FIFO 通知顺序,一些应用如 `Pool` 类可以使用这种具有公平性属性的信号量实现,但可能会带来额外的开销。
#### 3. 优先级
信号量实现类除了关注公平性,还可以关注线程优先级。`notify` 方法不保证按优先级处理线程,但在某些 JVM 实现中会这样做。
优先级设置通常在可运行线程数量远多于 CPU 数量,且线程中的任务具有不同紧急程度或重要性时才有价值,常见于嵌入式(软)实时系统。
然而,依赖优先级设置会使通知策略变得复杂,可能会出现优先级反转问题。当高优先级线程因等待低优先级线程完成并释放锁或改变条件而被阻塞时,就会发生优先级反转。
一种解决方案是使用特殊的信号量类或基于此类信号量构建的锁类,这些并发控制对象可以在高优先级线程被阻塞时,临时提高可能解除其阻塞的低优先级线程的优先级。但这种策略仅在使用严格优先级调度的特定 JVM 实现中有效,并且会牺牲程序的可移植性。
#### 4. 闩锁
闩锁(Latches)是一种最终接收一个不会再改变的值的变量或条件。二进制闩锁变量或条件(通常简称为闩锁,也称为一次性触发器)只能从初始状态改变一次到最终状态。
可以使用简单的 `Latch` 类来封装围绕闩锁的并发控制技术,该类遵循通常的 `acquire - release` 接口,且一次释放操作允许所有之前和之后的 `acquire` 操作继续进行。
以下是一个使用闩锁的示例,用于确保游戏玩家在游戏正式开始前等待:
```java
class Player implements Runnable {
protected final Latch startSignal;
Player(Latch l) { startSignal = l; }
public void run() {
try {
startSignal.acquire();
play();
} catch(InterruptedException ie) { return; }
}
}
class Game {
void begin(int nplayers) {
Latch startSignal = new Latch();
for (int i = 0; i < nplayers; ++i)
new Thread(new Player(startSignal)).start();
startSignal.release();
}
}
```
扩展形式的闩锁包括倒计时器,当固定数量的释放操作发生时,允许 `acquire` 操作继续进行。闩锁、倒计时器和基于它们构建的其他简单实用工具可用于协调对以下条件的响应:
- 完成指示:例如,强制一组线程等待直到其他活动完成。
- 时间阈值:例如,在特定日期触发一组线程。
- 事件指示:例如,在收到特定数据包或点击按钮后触发处理。
- 错误指示:例如,触发一组线程进行全局关闭任务。
#### 5. 闩锁变量和谓词
对于大多数一次性触发应用,实用类很方便,但闩锁字段(也称为永久变量)和谓词在其他情况下可以提高可靠性、简化使用并提高效率。
闩锁谓词(包括常见的阈值指示器特殊情况)是少数几种可以使用非同步忙等待循环(在受保护方法中)实现的条件之一。如果谓词是已知的闩锁谓词,则在检查其为真和后续需要其保持为真的操作之间,其值不会改变。以下是一个示例:
```java
class LatchingThermometer {
private volatile boolean ready;
private volatile float temperature;
public double getReading() {
while (!ready)
Thread.yield();
return temperature;
}
void sense(float t) {
temperature = t;
ready = true;
}
```
0
0
复制全文
相关推荐









