软件事务内存(Software Transactional Memory)详细解析
1. 事务与事务线程
事务的状态被封装在一个线程本地的
Transaction
对象中,该对象可以处于三种状态之一:
ACTIVE
、
ABORTED
或
COMMITTED
。当创建一个事务时,其默认状态为
ACTIVE
。为了方便处理未在事务中执行的线程,定义了一个常量
Transaction.COMMITTED
事务对象。
以下是
Transaction
类的代码:
public class Transaction {
public enum Status {ABORTED, ACTIVE, COMMITTED};
public static Transaction COMMITTED = new Transaction(Status.COMMITTED);
private final AtomicReference<Status> status;
static ThreadLocal<Transaction> local = new ThreadLocal<Transaction>() {
protected Transaction initialValue() {
return new Transaction(Status.COMMITTED);
}
};
public Transaction() {
status = new AtomicReference<Status>(Status.ACTIVE);
}
private Transaction(Transaction.Status myStatus) {
status = new AtomicReference<Status>(myStatus);
}
public Status getStatus() {
return status.get();
}
public boolean commit() {
return status.compareAndSet(Status.ACTIVE, Status.COMMITTED);
}
public boolean abort() {
return status.compareAndSet(Status.ACTIVE, Status.ABORTED);
}
public static Transaction getLocal() {
return local.get();
}
public static void setLocal(Transaction transaction) {
local.set(transaction);
}
}
线程可以通过调用
getStatus()
方法来测试其当前事务的状态。如果线程发现其当前事务已被中止,则会抛出
AbortedException
。线程还可以通过调用静态的
getLocal()
或
setLocal()
方法来获取和设置其当前事务。
TThread
(事务线程)类是标准 Java
Thread
类的子类。每个事务线程都有几个关联的处理程序:
-
onCommit
和
onAbort
处理程序分别在事务提交或中止时被调用。
- 验证处理程序在事务即将提交时被调用,它返回一个布尔值,指示线程的当前事务是否应该尝试提交。
doIt()
方法的执行步骤如下:
1. 创建一个新的
ACTIVE
事务。
2. 调用事务的
call()
方法。
3. 如果该方法抛出
AbortedException
,则
doIt()
方法会重试循环。
4. 任何其他异常意味着应用程序出现了错误,方法会抛出
PanicException
,打印错误消息并关闭所有操作。
5. 如果事务返回,
doIt()
调用验证处理程序来测试是否提交。
6. 如果验证成功,则尝试提交事务。
7. 如果提交成功,则运行提交处理程序并返回。
8. 否则,如果验证失败,它会显式中止事务。如果提交因任何原因失败,它会在重试之前运行中止处理程序。
2. 僵尸事务与一致性
同步冲突会导致事务中止,但在冲突发生后,并不总是能够立即停止事务的线程。这些僵尸事务可能会继续运行,即使它们已经不可能提交。这就引出了一个重要的设计问题:如何防止僵尸事务看到不一致的状态。
例如,一个对象有两个字段
x
和
y
,初始值分别为 1 和 2。每个事务都要保持
y
始终等于
2x
的不变性。事务
Z
读取
y
,看到值为 2。事务
A
将
x
和
y
分别更改为 2 和 4,并提交。此时,
Z
成为了僵尸事务,因为它继续运行,但永远不会提交。
Z
后来读取
x
,看到值为 2,这与它之前读取的
y
值不一致。
为了避免这种情况,TinyTM 保证所有事务,包括僵尸事务,都能看到一致的状态。
3. 原子对象
并发事务通过共享原子对象进行通信。原子对象的访问由一个标准化的接口提供,该接口提供了一组匹配的 getter 和 setter 方法。
以下是
AtomicObject
抽象类的代码:
public abstract class AtomicObject <T extends Copyable<T>> {
protected Class<T> internalClass;
protected T internallnit;
public AtomicObject(T init) {
internalInit = init;
internalClass = (Class<T>) init.getClass();
}
public abstract T openRead();
public abstract T openWrite();
public abstract boolean validate();
}
需要构造两个实现该接口的类:
- 顺序实现:不提供同步或恢复机制。
- 事务实现:提供同步和恢复机制。
顺序实现相对简单,对于每个匹配的 getter - setter 对,定义一个私有字段。同时,顺序实现需要满足
Copyable<T>
接口,该接口提供了一个
copyTo()
方法,用于将一个对象的字段复制到另一个对象。
以下是
SSkipNode
类的代码,它是
SkipNode
接口的顺序实现:
public class SSkipNode<T>
implements SkipNode<T>, Copyable<SSkipNode<T>> {
AtomicArray<SkipNode<T>> next;
int key;
T item;
public SSkipNode() {}
public SSkipNode(int level) {
next = new AtomicArray<SkipNode<T>>(SkipNode.class, level);
}
public SSkipNode(int level, int myKey, T myItem) {
this(level); key = myKey; item = myItem;
}
public AtomicArray<SkipNode<T>> getNext() {return next;}
public void setNext(AtomicArray<SkipNode<T>> value) {next = value;}
public int getKey() {return key;}
public void setKey(int value) {key = value;}
public T getItem() {return item;}
public void setItem(T value) {item = value;}
public void copyTo(SSkipNode<T> target) {
target.next = next;
target.key = key;
target.item = item;
}
}
4. 依赖或独立进度
事务内存的一个目标是让程序员不必担心饥饿、死锁等问题。然而,实现软件事务内存(STM)的人必须决定满足哪种进度条件。
目前的研究主要集中在较弱的依赖进度条件上,有两种方法有望实现良好的性能:
- 无阻塞的、无阻碍的 STM。
- 基于锁的、无死锁的 STM。
对于这两种 STM,冲突事务的进度由一个竞争管理器保证,它通过旋转或让步来决定何时延迟竞争线程,以便某些线程总能取得进展。
5. 竞争管理器
在 TinyTM 中,事务可以检测到何时即将导致同步冲突。此时,请求事务会咨询竞争管理器。竞争管理器作为一个决策机制,建议事务是立即中止另一个事务,还是暂停以允许另一个事务完成。
以下是
ContentionManager
抽象类的代码:
public abstract class ContentionManager {
static ThreadLocal<ContentionManager> local
= new ThreadLocal<ContentionManager>() {
protected ContentionManager initialValue() {
try {
return (ContentionManager) Defaults.MANAGER.newInstance();
} catch (Exception ex) {
throw new PanicException(ex);
}
}
};
public abstract void resolve(Transaction me, Transaction other);
public static ContentionManager getLocal() {
return local.get();
}
public static void setLocal(ContentionManager m) {
local.set(m);
}
}
常见的竞争管理器策略有:
-
Backoff
:事务
A
反复随机后退一段时间,直到达到某个限制,然后中止
B
。
-
Priority
:每个事务在开始时获取一个时间戳。如果
A
的时间戳比
B
旧,则中止
B
,否则等待。
-
Greedy
:每个事务在开始时获取一个时间戳。如果
A
的时间戳比
B
旧,或者
B
正在等待另一个事务,则
A
中止
B
。
-
Karma
:每个事务跟踪自己完成的工作量,完成更多工作的事务具有优先级。
以下是使用后退策略的竞争管理器实现:
public class BackoffManager extends ContentionManager {
private static final int MIN_DELAY = ...;
private static final int MAX_DELAY = ...;
Random random = new Random();
Transaction previous = null;
int delay = MIN_DELAY;
public void resolve(Transaction me, Transaction other) {
if (other != previous) {
previous = other;
delay = MIN_DELAY;
}
if (delay < MAX_DELAY) {
Thread.sleep(random.nextInt(delay));
delay = 2 * delay;
} else {
other.abort();
delay = MIN_DELAY;
}
}
}
6. 实现原子对象
线性化要求单个方法调用看起来是原子执行的。为了保证多个原子调用也具有相同的属性,我们需要考虑事务性原子对象的实现。
有两种替代方法来实现同步和恢复:
-
FreeObject
类是无阻碍的。
-
LockObject
类使用锁进行同步。
以下是
TSkipNode
类的代码,它是
SkipNode
接口的事务性实现:
public class TSkipNode<T> implements SkipNode<T> {
AtomicObject<SSkipNode<T>> atomic;
public TSkipNode(int level) {
atomic = new LockObject<SSkipNode<T>>(new SSkipNode<T>(level));
}
public TSkipNode(int level, int key, T item){
atomic = new LockObject<SSkipNode<T>>(new SSkipNode<T>(level, key, item));
}
public TSkipNode(int level, T item){
atomic = new LockObject<SSkipNode<T>>(new SSkipNode<T>(level, item.hashCode(), item));
}
public AtomicArray<SkipNode<T>> getNext() {
AtomicArray<SkipNode<T>> forward = atomic.openRead().getNext();
if (!atomic.validate())
throw new AbortedException();
return forward;
}
public void setNext(AtomicArray<SkipNode<T>> value) {
atomic.openWrite().setNext(value);
}
// getKey, setKey, getItem, and setItem omitted ...
}
TSkipNode
类的 getter 方法执行步骤如下:
1. 调用
openRead()
提取一个版本。
2. 调用该版本的 getter 提取字段值,并存储在局部变量中。
3. 调用
validate()
确保读取的值是一致的。
setter 方法的实现与之对称。
7. 无阻碍的原子对象
一个算法如果任何线程在单独运行足够长的时间后都能取得进展,那么它就是无阻碍的。以下是无阻碍的
AtomicObject
实现的概述:
每个对象有三个逻辑字段:
- 所有者字段:最后一个访问该对象的事务。
- 旧版本:该事务到达之前的对象状态。
- 新版本:反映该事务的更新(如果有的话)。
当事务
A
访问一个对象时:
1. 如果前一个所有者
B
是
COMMITTED
,则新版本是当前状态。
A
成为对象的当前所有者,将旧版本设置为前一个新版本,新版本设置为前一个新版本的副本(如果是 setter 调用)或新版本本身(如果是 getter 调用)。
2. 如果
B
是
ABORTED
,则旧版本是当前状态。
A
成为对象的当前所有者,将旧版本设置为前一个旧版本,新版本设置为前一个旧版本的副本(如果是 setter 调用)或旧版本本身(如果是 getter 调用)。
3. 如果
B
仍然是
ACTIVE
,则
A
和
B
冲突,
A
咨询竞争管理器,以决定是否中止
B
或暂停,给
B
一个完成的机会。
当
A
提交时,它调用
compareAndSet()
将其状态更改为
COMMITTED
。如果成功,提交完成;否则,它已被另一个事务中止。
下面是事务执行流程的 mermaid 流程图:
graph TD;
A[开始事务] --> B[创建ACTIVE事务];
B --> C[执行事务操作];
C --> D{是否抛出AbortedException};
D -- 是 --> B;
D -- 否 --> E{是否其他异常};
E -- 是 --> F[抛出PanicException];
E -- 否 --> G[调用验证处理程序];
G --> H{验证是否成功};
H -- 是 --> I[尝试提交事务];
I --> J{提交是否成功};
J -- 是 --> K[运行提交处理程序并返回];
J -- 否 --> L[运行中止处理程序并重试];
H -- 否 --> M[显式中止事务并重试];
总结来说,软件事务内存通过事务、原子对象和竞争管理器等机制,为并发编程提供了一种有效的同步和恢复方法,帮助程序员避免了传统锁机制带来的一些问题。不同的实现策略和算法可以根据具体的应用场景进行选择,以达到最佳的性能和一致性。
软件事务内存(Software Transactional Memory)详细解析
8. 无阻碍原子对象的详细操作流程
无阻碍原子对象的操作流程较为复杂,下面详细介绍事务在访问对象时的具体步骤以及状态变化。
当事务
A
访问一个对象时,具体操作如下:
1.
获取对象的当前所有者
B
:事务
A
首先会检查对象的所有者字段,确定当前对象的所有者是哪个事务。
2.
根据
B
的状态进行不同处理
:
-
若
B
为
COMMITTED
:
- 此时新的版本就是当前对象的状态。
- 事务
A
将自己设置为对象的当前所有者。
- 把旧版本设置为之前的新版本。
- 若此次调用是 setter 方法,新版本设置为之前新版本的副本;若是 getter 方法,新版本保持不变。
-
若
B
为
ABORTED
:
- 旧版本是当前对象的状态。
- 事务
A
成为对象的新所有者。
- 旧版本设置为之前的旧版本。
- 若为 setter 调用,新版本设置为之前旧版本的副本;若为 getter 调用,新版本为旧版本本身。
-
若
B
为
ACTIVE
:
- 说明事务
A
和
B
产生了冲突。
- 事务
A
会咨询竞争管理器,竞争管理器会给出建议,决定是中止事务
B
还是让事务
A
暂停,给事务
B
完成的机会。
-
事务提交操作
:
-
当事务
A要提交时,它会调用compareAndSet()方法将自己的状态从ACTIVE改为COMMITTED。 - 如果修改成功,说明事务提交完成,后续访问该对象的事务会将新的版本视为当前状态。
-
如果修改失败,意味着事务
A被其他事务中止了,后续访问该对象的事务会将旧版本视为当前状态。
-
当事务
9. 不同竞争管理器策略的对比
不同的竞争管理器策略在处理事务冲突时有着不同的表现,下面通过表格对比几种常见的竞争管理器策略:
| 策略名称 | 策略描述 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- |
| Backoff | 事务
A
反复随机后退一段时间,直到达到某个限制,然后中止
B
| 实现简单,能在一定程度上避免冲突 | 可能会浪费大量时间在后退等待上 |
| Priority | 每个事务在开始时获取一个时间戳。如果
A
的时间戳比
B
旧,则中止
B
,否则等待 | 保证了先开始的事务有更高的优先级,避免长事务饥饿 | 可能会导致新事务长时间等待 |
| Greedy | 每个事务在开始时获取一个时间戳。如果
A
的时间戳比
B
旧,或者
B
正在等待另一个事务,则
A
中止
B
| 消除了事务等待链,提高了整体效率 | 可能会频繁中止其他事务,增加系统开销 |
| Karma | 每个事务跟踪自己完成的工作量,完成更多工作的事务具有优先级 | 考虑了事务的实际工作量,更公平 | 工作量的衡量标准较难确定 |
10. 原子对象实现的性能分析
不同的原子对象实现方式在性能上有不同的特点,下面分析
FreeObject
类和
LockObject
类的性能:
-
FreeObject
类(无阻碍实现)
:
-
优点
:在没有同步冲突的情况下,线程可以独立快速地执行,不会被其他线程阻塞,具有较好的并发性能。
-
缺点
:当出现同步冲突时,需要依赖竞争管理器来解决冲突,可能会导致一些线程频繁重试,增加系统开销。
-
LockObject
类(基于锁的实现)
:
-
优点
:通过锁机制可以有效地避免同步冲突,保证数据的一致性,实现相对简单。
-
缺点
:锁的使用可能会导致线程阻塞,降低并发性能,并且可能会出现死锁等问题。
在选择原子对象的实现方式时,需要根据具体的应用场景和性能需求来决定。如果应用场景中同步冲突较少,对并发性能要求较高,可以选择
FreeObject
类;如果同步冲突较多,对数据一致性要求较高,可以选择
LockObject
类。
11. 软件事务内存的应用场景
软件事务内存适用于多种并发编程场景,以下是一些常见的应用场景:
1.
数据库操作
:在数据库中,多个事务可能会同时对数据进行读写操作。使用软件事务内存可以保证事务的原子性和一致性,避免数据冲突和不一致的问题。例如,在银行系统中,多个用户可能同时进行转账操作,通过软件事务内存可以确保转账操作的原子性,避免出现数据错误。
2.
并行算法
:在并行算法中,多个线程可能会同时访问和修改共享数据。软件事务内存可以提供一种简单有效的同步机制,让程序员不必担心线程安全问题,专注于算法的实现。例如,在并行排序算法中,多个线程可以同时对数组的不同部分进行排序,通过软件事务内存可以保证排序过程的正确性。
3.
多线程游戏开发
:在多线程游戏开发中,多个线程可能会同时处理游戏中的不同元素,如角色移动、碰撞检测等。使用软件事务内存可以确保这些操作的原子性和一致性,避免游戏出现异常情况。例如,在多人在线游戏中,多个玩家的操作可能会同时影响游戏世界的状态,软件事务内存可以保证游戏状态的正确更新。
12. 软件事务内存的未来发展趋势
随着计算机技术的不断发展,软件事务内存也在不断演进。以下是一些可能的未来发展趋势:
1.
性能优化
:研究人员将继续探索更高效的算法和实现方式,以提高软件事务内存的性能。例如,开发更智能的竞争管理器,减少冲突解决的开销;优化原子对象的实现,提高并发性能。
2.
与硬件的结合
:未来可能会出现专门支持软件事务内存的硬件,通过硬件加速来提高软件事务内存的性能。例如,在 CPU 中集成事务内存单元,减少软件实现的开销。
3.
更广泛的应用
:随着软件事务内存的性能和可靠性不断提高,它将在更多的领域得到应用。例如,在人工智能、大数据处理等领域,软件事务内存可以提供一种有效的并发编程解决方案。
13. 总结与建议
软件事务内存为并发编程提供了一种有效的同步和恢复方法,通过事务、原子对象和竞争管理器等机制,帮助程序员避免了传统锁机制带来的一些问题。在实际应用中,需要根据具体的场景和需求选择合适的实现策略和算法。
以下是一些使用软件事务内存的建议:
1.
了解应用场景
:在选择软件事务内存的实现方式之前,需要充分了解应用场景的特点,如并发程度、同步冲突的频率等,以便选择最适合的实现方式。
2.
合理选择竞争管理器
:不同的竞争管理器策略有不同的优缺点,需要根据应用场景选择合适的竞争管理器。例如,在同步冲突较少的场景中,可以选择简单的 Backoff 策略;在同步冲突较多的场景中,可以选择 Greedy 或 Karma 策略。
3.
进行性能测试
:在实际应用中,需要对不同的实现方式进行性能测试,以确定最佳的实现方案。可以使用性能测试工具,如 JMH 等,对软件事务内存的性能进行评估。
下面是一个总结软件事务内存关键组件和操作的表格:
| 组件/操作 | 描述 |
| ---- | ---- |
| 事务 | 封装事务的状态,有
ACTIVE
、
ABORTED
、
COMMITTED
三种状态,通过
Transaction
类实现 |
| 原子对象 | 并发事务通信的共享对象,有顺序实现和事务实现,通过
AtomicObject
抽象类定义接口 |
| 竞争管理器 | 解决同步冲突,有多种策略可供选择,通过
ContentionManager
抽象类实现 |
| 事务执行 | 通过
doIt()
方法执行事务操作,包括创建事务、执行操作、验证、提交等步骤 |
通过以上的介绍和分析,希望能帮助读者更好地理解软件事务内存的原理、实现和应用,在并发编程中选择合适的解决方案。
详细解析&spm=1001.2101.3001.5002&articleId=150385151&d=1&t=3&u=5f88e7885abd43fba63a3e9b942bf4e9)
210

被折叠的 条评论
为什么被折叠?



