42、软件事务内存(Software Transactional Memory)详细解析

软件事务内存(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 完成的机会。

  1. 事务提交操作
    • 当事务 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() 方法执行事务操作,包括创建事务、执行操作、验证、提交等步骤 |

通过以上的介绍和分析,希望能帮助读者更好地理解软件事务内存的原理、实现和应用,在并发编程中选择合适的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值