在不影响性能或吞吐量的情况下对所有线程具有完全原子性

时间:2014-09-02 02:17:16

标签: java multithreading thread-safety atomicity

我有一个主机名列表,我应该通过从中发出正确的URL来拨打电话。如果我在链表中​​有四个主机名(hostA,hostB,hostC,hostD),那就说吧了 -

  • 执行hostA url,如果hostA为UP,则获取数据并返回响应。
  • 但是如果hostA关闭,则将hostA添加到阻止主机名列表中,并确保没有其他线程正在调用hostA。然后尝试执行hostB url并返回响应。
  • 但是如果hostB也关闭了,那么也将hostB添加到阻止主机名列表中并重复同样的事情。

另外,我在我的应用程序中运行了一个后台线程,它将包含块主机名列表(来自我的另一个服务),我们不应该拨打电话,但它每10分钟运行一次,因此块主机名列表将会仅在10分钟后才更新,所以如果存在任何主机名阻止列表,那么我不会从主线程调用该主机名,我将尝试调用另一个主机名。意味着如果hostA被阻止,则阻止列表中会显示hostA,但如果hostA已启用,则该列表中不会包含hostA

下面是我的后台线程代码,它从我的服务URL获取数据,并在应用程序启动后每10分钟继续运行。然后,它将解析来自URL的数据并将其存储在ClientData类变量 -

中 的 TempScheduler
public class TempScheduler {

    // .. scheduledexecutors service code to start the background thread

    // call the service and get the data and then parse 
    // the response.
    private void callServiceURL() {
        String url = "url";
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject(url, String.class);
        parseResponse(response);
    }

    // parse the response and store it in a variable
    private void parseResponse(String response) {
        //...       

        // get the block list of hostnames
        Map<String, List<String>> coloExceptionList = gson.fromJson(response.split("blocklist=")[1], Map.class);
        List<String> blockList = new ArrayList<String>();
        for(Map.Entry<String, List<String>> entry : coloExceptionList.entrySet()) {
            for(String hosts : entry.getValue()) {
                blockList.add(hosts);
            }
        }

        // store the block list of hostnames which I am not supposed to make a call
        ClientData.replaceBlockedHosts(blockList);
    }
}

以下是我的ClientData课程。 replaceBlockedHosts方法只能由后台线程调用,这意味着只有一个编写器。但是主应用程序线程将多次调用isHostBlocked方法来检查特定主机名是否被阻止。并且blockHost方法也将从catch block多次调用以在blockedHosts列表中添加向下主机,因此我需要确保所有读取线程都能看到一致的数据而不是调用该主机,而不是调用主机名链表中的下一个主机。

ClientData
public class ClientData {

    // .. some other variables here which in turn used to decide the  list of hostnames

    private static final AtomicReference<ConcurrentHashMap<String, String>> blockedHosts = 
            new AtomicReference<ConcurrentHashMap<String, String>>(new ConcurrentHashMap<String, String>());

    public static boolean isHostBlocked(String hostName) {
        return blockedHosts.get().containsKey(hostName);
    }

    public static void blockHost(String hostName) {
        blockedHosts.get().put(hostName, hostName);
    }

    public static void replaceBlockedHosts(List<String> hostNames) {
        ConcurrentHashMap<String, String> newBlockedHosts = new ConcurrentHashMap<>();
        for (String hostName : hostNames) {
            newBlockedHosts.put(hostName, hostName);
        }
        blockedHosts.set(newBlockedHosts);
    }
}

下面是我的主要应用程序线程代码,其中我有我应该打电话的主机名列表。如果hostname为空或在阻止列表类别中,则我不会调用该特定主机名,并将在列表中尝试下一个主机名。

@Override
public DataResponse call() {

    List<String> hostnames = new LinkedList<String>();

    // .. some separate code here to populate the hostnames list
    // from ClientData class

    for (String hostname : hostnames) {     

        // If host name is null or host name is in block list category, skip sending request to this host
        if (hostname == null || ClientData.isHostBlocked(hostname)) {
            continue;
        }

        try {
            String url = generateURL(hostname);

            response = restTemplate.getForObject(url, String.class);

            break;
        } catch (RestClientException ex) {
            // add host to block list, 
            // Is this call fully atomic and thread safe for blockHost method 
            // in ClientData class?
            ClientData.blockHost(hostname);
        }
    }
}

每当主机名从主线程关闭时,我都不需要调用主机名。我的后台线程也从我的一个服务中获取这些细节,每当任何服务器关闭时,它将具有作为块主机的主机名列表,并且无论何时启动,该列表都将得到更新。

而且,无论何时抛出任何RestClientException,我都会在blockedHosts concurrentmap中添加该主机名,因为我的后台线程每10分钟运行一次,这样地图就不会有这个主机名直到10分钟完成。每当此服务器恢复时,我的背景将自动更新此列表。

我上面的主机名阻止列表代码是完全原子的还是线程安全的?因为我想要的是 - 如果hostA关闭,那么在更新被阻止的主机列表之前,没有其他线程应该调用hostA。

2 个答案:

答案 0 :(得分:1)

请记住,与其他主机的通信所花费的时间远远超过您在线程中所做的任何事情。在这种情况下,我不担心原子操作。

假设我们有线程t1t2t1hostA发送请求并等待回复。达到超时后,将抛出RestClientException。现在,在抛出异常并将该主机添加到被阻止的主机列表之间存在非常小的时间跨度。 可能发生t2尝试在主机被阻止之前向hostA发送请求 - 但t2已经更有可能在很长一段时间内发送t1等待回复,你无法阻止。

您可以尝试设置合理的超时时间。当然还有其他类型的错误没有等待超时,但即使是那些时间比处理异常还多。

使用ConcurrentHashMap是线程安全的,应该足以跟踪被阻止的主机。

除非你使用AtomicReference之类的方法,否则compareAndSet本身并没有太大作用,因此调用不是原子的(但如上所述,不需要在我看来)。如果您确实想在遇到异常后立即阻止主机,则应使用某种同步。您可以使用synchronized set来存储被阻止的主机。这仍然无法解决实际检测到任何连接错误需要一些时间的问题。


关于更新:正如评论中所述,Future timeout应该大于请求超时。否则,Callable可能会被取消,主机将不会被添加到列表中。使用Future.get时甚至可能不需要超时,因为请求最终会成功或失败。

当主机A出现故障时,您看到许多异常的实际问题可能就是许多线程仍在等待主机A的响应。您只能在启动请求之前检查被阻止的主机,而不是在任何请求期间。仍在等待来自该主机的响应的任何线程将继续这样做,直到达到超时。

如果您想阻止这种情况,您可以尝试定期检查当前主机是否尚未阻止。这是一个非常天真的解决方案,因为它基本上是民意调查,所以它会破坏期货的目的。它应该有助于理解一般问题。

// bad pseudo code 

DataTask dataTask = new DataTask(dataKeys, restTemplate);
future = service.submit(dataTask);

while(!future.isDone()) {
    if( blockedHosts.contains(currentHost) ) {
        // host unreachable, don't wait for http timeout
        future.cancel(); 
    }
    thread.sleep(/* */);
}

更好的方法是向所有正在等待相同主机的DataTask线程发送中断,这样它们就可以中止请求并尝试下一个主机。

答案 1 :(得分:1)

当您将ConcurrentHashMap放入AtomicReference时,您的操作的原子性不会改变。无论如何putget都是原子的,唯一受影响的操作replaceBlockedHosts也适用于简单的volatile引用。但我不知道为什么你需要这个。


call()方法中的内容是 check-then-act 模式:

首先,你致电ClientData.isHostBlocked(hostname) 然后你拨打restTemplate.getForObject(generateURL(hostname), …)

因此blockHostisHostBlocked的原子性确实会阻止线程在isHostBlocked调用后在另一个线程调用blockHost时正确。因此,在后者将主机添加到阻止列表后,前者仍将继续进行网络操作。


如果要限制在同一主机上可能失败的线程数,则必须限制访问同一主机的线程数。没有办法解决它。