【问题标题】:Round robin scheduling algorithm in Java using AtomicBooleanJava中使用AtomicBoolean的循环调度算法
【发布时间】:2015-06-19 04:57:11
【问题描述】:

当我向外部系统发送请求时,我想实施严格的循环调度。有两个外部系统服务器。第一个请求应该发送到“System1”,第二个请求必须发送到“System2”,下一个请求必须发送到“System1”,依此类推。

由于我只有两台服务器可以向其发送请求,并且我希望在没有任何阻塞和上下文切换的情况下获得最大性能,因此我选择了 AtomicBoolean,因为它使用了 CAS 操作。

我的实现类

1. RoundRobinTest.java

package com.concurrency;

import java.util.Iterator;

public class RoundRobinTest 
{
    public static void main(String[] args) 
    {
        for (int i = 0; i < 500; i++) 
        {
            new Thread(new RoundRobinLogic()).start();
        }
        try 
        {
            // Giving a few seconds for the threads to complete
            Thread.currentThread().sleep(2000);
            Iterator<String> output = RoundRobinLogic.output.iterator();
            int i=0;
            while (output.hasNext()) 
            {
                System.out.println(i+++":"+output.next());
                // Sleeping after each out.print 
                Thread.currentThread().sleep(20);
            }
        } 
        catch (Exception ex) 
        {
            // do nothing
        }
    }

}

2.RoundRobinLogic.java(具有静态 AtomicBoolean 对象的类)

package com.concurrency;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;

public class RoundRobinLogic implements Runnable 
{
    private static AtomicBoolean bool = new AtomicBoolean(true);

    public static Queue<String> output = new ConcurrentLinkedDeque<>();

    @Override
    public void run() 
    {
        if(bool.getAndSet(false))
        {
            // Sending the request to first system
            output.add("Request to System1");
        }
        else if(!bool.getAndSet(true))
        {
            // Sending the request to first system
            output.add("Request to System2");
        }       
    }

}

输出:


......................
314:Request to System1
315:Request to System2
316:Request to System1
317:Request to System2
318:Request to System1
319:Request to System1
320:Request to System2
321:Request to System2
322:Request to System1
323:Request to System2
324:Request to System1
325:Request to System2
......................

请求 318 和 319 已发送到同一台服务器,在这种情况下 AtomicBoolean 失败。对于我的应用程序,一次可能有 1000-2000 个线程访问共享对象。 从 Java 并发实践中,我看到了以下内容。

在竞争激烈的情况下,锁定往往优于原子变量,但在更现实的情况下 争用级别原子变量优于锁。这是因为锁通过挂起线程对争用做出反应, 减少共享内存总线上的 CPU 使用率和同步流量。 对于低到中等的争用,原子提供了更好的可扩展性;竞争激烈,锁提供 更好地避免争用。 (基于 CAS 的算法在单 CPU 系统上也优于基于锁的算法,因为 CAS 总是在单个 CPU 系统上成功,除非在极少数情况下线程在中间被抢占。 读修改写操作。)

现在我有以下问题。

  1. 有没有其他高效的非阻塞方式,实现轮询请求发送。
  2. 在激烈的争论中,AtomicBoolean 是否有可能失败?我的理解是,性能/吞吐量可能会由于激烈的争用而下降。但是在上面的例子中 AtomicBoolean 失败了。为什么 ?

【问题讨论】:

  • 你有 1k-2k 线程同时运行?你在多少个处理器上运行它?
  • boolqueue 的访问不同步,即使它们的状态看起来相互依赖。
  • 只是一个想法,如果这真的是你想要的,而且它不是对真正问题的简化,偶尔出现不规则是可以的,因为大量的不规则会均匀化并且您将在两台服务器上获得相同的负载。我喜欢完美,但有时,它根本不需要。现在你可以回去尝试把这件事做好了;)
  • @MickMnemonic 刚刚给了你答案。
  • 您的 run 方法没有利用 CAS。考虑一下:由于时间问题,可能不会在您的run 方法中调用output.add

标签: java multithreading algorithm atomic atomicboolean


【解决方案1】:

假设您使用的不是Queue,而是实际系统的api。我看到的问题与:

    if(bool.getAndSet(false))
    {
        // Sending the request to first system
        output.add("Request to System1");
    }
    else if(!bool.getAndSet(true))
    {
        // Sending the request to second system
        output.add("Request to System2");
    }     

如果两个条件都失败了怎么办?这怎么可能?想象一下,在输入第一个 if 时,布尔值是 true。然后您尝试将其设置为 false,但另一个线程击败了您,因此您看到了false。然后你试试else if。现在如果你到达那里时的else if 是假的,但设置为真,买另一个线程怎么办?在这种情况下,两次尝试都会失败。

我会将其重构为:

while(true){
  boolean current = bool.get();
  if(bool.compareAndSet(current, !current){
     if(current){ 
        //send request to first system
     } else {
        //send request to second system
     }
     return;
  }
}

正如 Sean Bright 所提到的,因为您正在添加到队列中,即使您像我上面那样实现它,您仍然可能会看到一些乱序的值,因为队列本身不是与 AtomicBoolean 同步的一部分。

【讨论】:

  • 我开始研究自己的答案,但你的答案涵盖了广泛的内容。我认为重要的是要提到 add()ed 到 Queue 的结果不一定是 OP 希望看到的 1、2、1、2 等顺序。完成这项工作需要更多的同步。
  • 我认为current!current返回的值不是原子操作的一部分。
  • @1010 你为什么这么认为?
  • @SeanBright 这是一个公平的观点。 1010 应该将其添加为答案。我不会劫持是答案。
  • @Albin 你是对的。从概率的角度来看,与 SeanBright 的解决方案相比,您在这里看到比赛失败的可能性要大得多。我认为他的更好,tbh。话虽这么说,我会继续回答以说明您的比赛条件。
【解决方案2】:

除了约翰的回答之外,RoundRobinLogic 的更简洁且可能稍微更有效的实现将使用AtomicIntegerAtomicLong。这消除了将AtomicBoolean 的当前值与新值进行比较的需要:

class RoundRobinLogic implements Runnable
{
    private static final AtomicInteger systemIndex = new AtomicInteger(1);

    public static final Queue<String> output = new ConcurrentLinkedDeque<>();

    @Override
    public void run()
    {
        if (systemIndex.incrementAndGet() % 2 == 0) {
            // Sending the request to first system
            output.add("Request to System1");
        } else {
            // Sending the request to second system
            output.add("Request to System2");
        }
    }
}

这将使您可以相当轻松地将其扩展到其他系统:

class RemoteSystem
{
    private final String name;

    RemoteSystem(String name)
    {
        this.name = name;
    }

    public String name()
    {
        return name;
    }
}

class RoundRobinLogic implements Runnable
{
    private static final AtomicInteger systemIndex = new AtomicInteger(1);

    private static final RemoteSystem[] systems = new RemoteSystem[] {
        new RemoteSystem("System1"),
        new RemoteSystem("System2"),
        new RemoteSystem("System3"),
        new RemoteSystem("System4"),
    };

    public static final Queue<String> output = new ConcurrentLinkedDeque<>();

    @Override
    public void run()
    {
        RemoteSystem system = systems[systemIndex.incrementAndGet() % systems.length];

        // Sending the request to right system
        output.add("Request to " + system.name());
    }
}

【讨论】:

  • 为了让请求按照 OP 的要求完美交替,incrementAndGetoutput.add 不应该同步吗?
  • @1010 他们应该是,这就是为什么 OP 在不阻塞的情况下无法获得他想要的确切输出。我希望 OP 以代码为例。
  • 对...如果对 system1 的请求比对 system2 的请求花费的时间更长,会发生什么?现在它只是一个固定的字符串,但实际上它会打开一个套接字,做一些 I/O 等。这里介绍的方法只能确保“大约一半”的请求到达两个系统中的每一个.
  • @Albin,您调用的是队列还是外部系统并不重要。除非您同步这两个方法调用,否则无法保证服务调用之间的完美交替(我认为这是您问题的重点)。
  • 正确。不能保证“完美交替”。上面的代码唯一保证的是,给定n 外部系统和x 请求,x / n 请求将发送到每个系统。正如我对您的回答所评论的那样,使用内部锁包装对外部系统的调用将显着降低吞吐量。
【解决方案3】:

因为您的要求基本上是:实现一个原子操作

  1. 评估和翻转布尔值(或在通用 n 服务器案例中评估模数并增加计数器)
  2. 根据步骤 1 的结果将条目插入队列,

您无法通过使第 1 步和第 2 步单独线程安全来真正实现这一点;您必须将第 1 步和第 2 步同步

这是一个应该可以工作的简单实现:

import java.util.LinkedList;
import java.util.Queue;

public class RoundRobinLogic implements Runnable 
{
    private static boolean bool = true;
    public static final Queue<String> OUTPUT = new LinkedList<String>();
    private static final Object LOCK = new Object();

    @Override
    public void run() {
        synchronized (LOCK) {
            OUTPUT.add(bool ? "Request to System1" : "Request to System2");
            bool = !bool;
        }
    }
}

关于您的问题:

  1. 如果您需要同时同步两个高于处理器级别的操作,则无法避免阻塞。 java.util.concurrent.atomic 中的类使用机器级原子指令,这就是使用这些类的代码(通常取决于平台)不需要阻塞的原因。
  2. 在您的实施中,AtomicBoolean 没有失败。相反,在读取布尔值和将元素添加到队列之间存在竞争条件。

【讨论】:

  • 虽然这将满足output 中的Strings 以交替顺序排列的人为要求,但使用由所有线程共享的内在锁将引入更多争用。将OUTPUT.add(bool...) 替换为OUTPUT.add(someHttpClient.makeRequest(...)),事情很快就会失控。将OUTPUT 设为Queue&lt;Future&lt;String&gt;&gt; 也是一种选择。
  • 这怎么是人为的要求? OP 要求进行“严格的循环赛”。他最初的解决方案似乎在绝大多数情况下都能正确地进行修改,所以如果严格性不是真正的要求,我真的不明白这个问题。
  • OP 多次表示output.add() 将被外部系统调用所取代。而且我不知道如何更清楚地说明这一点,但是如果您的OUTPUT.add 被对外部系统的调用所取代,那么所有其他线程都会在内部锁上阻塞,这意味着只有一个请求可能是一次发生。所以是的,您的代码将“工作”,因为Queue 的顺序正确,但这并不是OP 真正想要的。 @Albin,请随时在这里插话。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-26
  • 2017-01-18
  • 1970-01-01
  • 2014-01-18
  • 1970-01-01
  • 1970-01-01
  • 2019-01-08
相关资源
最近更新 更多