【问题标题】:Peterson algorithm in Java?Java中的彼得森算法?
【发布时间】:2011-02-24 02:39:34
【问题描述】:

有在Java中实现互斥的Peterson算法的示例实现吗?

【问题讨论】:

  • 请不要犹豫,发布您自己的答案,我正在寻找最好的答案!
  • 您的意思是彼得森的算法,用于仅使用具有强假设的共享内存实现互斥?我怀疑 Java 的内存模型(或任何其他足以显式提供内存模型的现代语言)是否允许 Peterson 的算法工作......如果您要使用显式内存屏障,为什么不使用该语言提供的同步指令?
  • @Pascal Cuoq 我不确定,你能指出我关于这个算法的原始论文,所以我可以检查一下吗?内存模型语义必须满足哪些前提条件才能保证算法的正确性?不使用Java提供的并发和同步机制的原因只是我试图理解Petersons算法,而不是Java中的并发编程。
  • @Gabriel 该算法是如此之短(原始文章以只有 2 页而闻名)以至于它完全在 Wikipedia 页面上:en.wikipedia.org/wiki/Peterson%27s_algorithm。也请参见“注意”部分。
  • @Gabriel 你是对的,问题是内存访问重新排序。这篇文章早于多 CPU/多核的优化实现。 DEC Alpha 设计者率先削弱了所有处理器/内核/线程始终查看单个共享内存的想法。这种“单一一致的共享内存”假设在文章中是隐含的(但明确提出)。

标签: java algorithm concurrency thread-safety memory-model


【解决方案1】:

这里没有人在 Java 中提供此算法的正确/安全实现。我不确定 John W 的解决方案应该如何工作,因为它缺少部分(即 ThreadLocals 的声明和对他的数组中应该包含的内容的解释 - 原始 booleans 没有 get()set())。

Chapter 17 of the Java Language Specification 解释了 Java 内存模型。特别感兴趣的是Section 17.4.5,它描述了 happens-before 顺序。在单个线程中很容易考虑。考虑 sn-p:

int x, y, z, w;
x = 0;
y = 5;

z = x;
w = y;

每个人都会同意,在这个 sn-p 结束时,xz 都等于 0yw 都等于 5。忽略声明,我们在这里有六个操作:

  1. 写信给x
  2. 写信给y
  3. 来自x的读取
  4. 写信给z
  5. 来自y的读取
  6. 写信给w

因为它们都出现在同一个线程中,JLS 说这些读取和写入保证表现出这种顺序:上面的每个动作 n (因为动作在单个线程中)都有一个与所有操作 mm > n 的发生前关系。

但是不同的线程呢? 对于正常的字段访问,线程之间没有建立之前发生的关系。这意味着线程 A 可以增加共享变量,线程 B 可能会读取该变量,但 看不到新值。在 JVM 中程序的执行过程中,线程 A 的写入传播可能已被重新排序,以发生在线程 B 的读取之后。

事实上,线程 A 可以写入变量 x,然后写入变量 y,从而在线程 A 中的这两个操作之间建立起先发生关系。但线程 B 可能会读取 x 和 @ 987654346@,B 在x 的新值出现之前获取y 的新值是合法的。规范说:

更具体地说,如果两个动作 分享发生前的关系, 他们不一定要出现 以这种顺序发生在任何 他们不共享的代码 发生之前的关系。

我们如何解决这个问题?对于普通的字段访问,volatile 关键字就足够了:

对 volatile 变量的写入 (§8.3.1.4) v 与所有同步 任何线程对 v 的后续读取 (其中后续根据定义 同步顺序)。

synchronizes-with 是比happens-before 更强的条件,并且由于happens-before 是可传递的,如果线程A 希望线程B 看到它对xy 的写入,它只需在写入xy 后写入一个易失性变量z。线程 B 需要在读取 xy 之前从 z 读取,并且可以保证看到 xy 的新值。

在 Gabriel 的解决方案中,我们看到了这种模式:in 发生写入,其他线程不可见,但随后turn 发生写入,因此其他线程可以保证看到这两个写入只要他们首先阅读turn

不幸的是,while 循环的条件是向后的:为了保证线程不会看到in 的陈旧数据,while 循环应该首先从turn 读取:

    // ...
    while (turn == other() && in[other()]) {
        // ...

考虑到这个修复,解决方案的其余大部分都可以:在临界区,我们不关心数据的陈旧性,因为,好吧,我们处于临界区!唯一的其他缺陷出现在最后:Runnable 将 in[id] 设置为新值并退出。是否保证其他线程看到in[id] 的新值?规范说不:

线程 T1 中的最后一个动作 与任何动作同步 另一个检测到 T1 的线程 T2 已终止。 T2可以完成 通过调用 T1.isAlive() 或 T1.join().

那么我们该如何解决呢?只需在方法末尾添加另一个写入turn

    // ...
    in[id] = false;
    turn = other();
}
// ...

由于我们对 while 循环进行了重新排序,因此其他线程将保证看到 in[id] 的新 false 值,因为对 in[id] 的写入发生在对 turn 的写入之前发生在对 @987654373 的读取之前@happens-before 从in[id] 读取。

不用说,如果没有 ton 的 cmets 度量,这种方法很脆弱,有人可能会出现并改变某些东西并巧妙地破坏正确性。仅仅声明数组volatile 是不够的:正如Bill Pugh 解释的in this thread(Java 内存模型的lead researchers 之一),声明数组volatile 会更新数组reference 对其他线程可见。数组元素的更新不一定是可见的(因此我们只需要通过使用另一个 volatile 变量来保护对数组元素的访问来跳过所有循环)。

如果您希望代码清晰简洁,请保持原样并将in 更改为AtomicIntegerArray(使用 0 表示 false,1 表示 true;没有 AtomicBooleanArray)。这个类就像一个数组,其元素都是volatile,因此可以很好地解决我们所有的问题。或者,您可以只声明两个 volatile 变量 boolean in0boolean in1,并更新它们而不是使用布尔数组。

【讨论】:

  • 哦,谢谢,这可能是我在 StackOverflow 上得到的最佳答案。这是非常有趣和鼓舞人心的,我想我会看看一些并发的Java API。
  • 有时书太好了,他们会取昵称。 “PickAx Book”、“K&R C”和“Dinosaur Book”是一些。 ““培训手册”(javaconcurrencyinpractice.com) 也不例外。它涵盖了内存模型和并发性的基础知识,然后非常详细地介绍了 java.util.concurrent 的内容,同时保持了出色的可读性。如果你想了解更多关于 java.util.concurrent 中的精彩工具,拿起它或向朋友借用它。
  • 这个答案太棒了。到底是谁投了反对票,为什么?恕我直言,在没有某种形式的评论的情况下投反对票是不礼貌的。
【解决方案2】:

你真的应该看看《多处理器编程的艺术》这本书。他详细介绍了不同的锁实现(自旋和阻塞)。他还讨论了其他不同类型的并发算法(例如跳过列表)。这是他关于 Peterson Lock 算法的书中的一个 sn-p

Class Peterson implements Lock{
   private final boolean []flag = new boolean[2];
   private volatile int victim;

   public void lock(){
        int i = ThreadID.get(); // ThreadID is a ThreadLocal field
        int j= 1 - i;
        flag[i] = true;    // I'm Interested
        victim = i;        // You go first
        while(flag[j] && victim == i){}; //wait
   }
   public void unlock(){
       int i = ThreadID.get();
       flag[i] = false;      //Not interested anymore
   }
 }

【讨论】:

  • 感谢您的建议。这与我发布的内容有什么不同吗?
  • 除了 Lock 实现之外,我没有看到任何真正的区别。你的看起来是正确的,我发布这个是为了显示为一个锁,并指出我建议的书的良好参考
  • WI 明白,谢谢,我很高兴做对了,尽管正如问题中的 cmets 所述,编译器违反了重要的先决条件,这使得该算法对于 Java 内存模型语义不正确。跨度>
【解决方案3】:

虽然不是 paterson 算法,但 AtomicBoolean 和 Atomic* 类使用无锁、繁忙循环的方法来更新共享数据。它们可能符合您的要求。

【讨论】:

    【解决方案4】:

    除非您对 Peterson 算法有特殊需求(在使用 Java 等高级语言时会很奇怪),否则我建议您查看该语言中内置的同步工具。

    例如,您可能会发现本书的章节“比赛条件和 互斥”在 Java 中很有用:http://java.sun.com/developer/Books/performance2/chap3.pdf

    特别是:

    Java 提供内置支持 等待这种“状态变化”通过 条件的概念。一个条件 然而,这有点用词不当, 因为这完全取决于用户 一个条件是否实际 发生了。此外,一个条件 不需要特别真实或 错误的。要使用条件,必须 熟悉三个关键方法 对象类:

    • wait():这个 方法用于等待条件。 当前有锁时调用 为特定(共享)而持有 对象。

    • notify():这个方法是 用于通知单个线程一个 条件已经(可能)改变了。 同样,当一个 锁目前正在持有一个 特定对象。只有一个 线程可以由于以下原因而被唤醒 这个电话。

    • notifyAll():此方法 用于通知多个线程 条件有(可能) 改变了。所有正在运行的线程 在调用此方法时将 得到通知。

    【讨论】:

    • 正如我在对问题的评论中提到的,这是特定于算法的,所以你没有回答我的问题,但是我感谢你的努力和好的报价,谢谢:)
    • 学习计算机科学的人需要知道锁定算法是如何工作的,而无需求助于高级语言功能。像分布式计算或操作系统这样的类确实需要我们知道锁机制是如何工作的,而且我们经常需要用任何语言来实现这样的算法。
    【解决方案5】:

    我自己在网上找不到,所以我决定试着写一下:

    
    public class Peterson implements Runnable {
    
        private static boolean[] in = { false, false };
        private static volatile int turn = -1;
    
        public static void main(String[] args) {
            new Thread(new Peterson(0), "Thread - 0").start();
            new Thread(new Peterson(1), "Thread - 1").start();
        }
    
        private final int id;
    
        public Peterson(int i) {
            id = i;
        }
    
        private int other() {
            return id == 0 ? 1 : 0;
        }
    
        @Override
        public void run() {
            in[id] = true;
            turn = other();
            while (in[other()] && turn == other()) {
                System.out.println("[" + id + "] - Waiting...");
            }
            System.out.println("[" + id + "] - Working ("
                    + ((!in[other()]) ? "other done" : "my turn") + ")");
            in[id] = false;
        }
    }
    
    

    欢迎评论,不胜感激:)

    【讨论】:

    • 您需要查看 Java 内存模型 (is.gd/cpKfw) 中的相关部分。写入数组元素不会建立任何类型的发生前关系,因此无法保证 Thread1 会看到 Thread0 将 [0] 中的设置为真。它甚至不适用于 volatile (is.gd/cpKpY)。也许尝试使用 AtomicIntegerArray 重写它,如 (is.gd/cpKrf ),使用 0 和 1 而不是 true 和 false。
    猜你喜欢
    • 2013-02-17
    • 2017-05-05
    • 2014-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-20
    • 1970-01-01
    相关资源
    最近更新 更多