1

我正在尝试实现锁定,当我在我的三个地图上进行写入时,我希望通过该锁定来避免发生读取。所以我的要求是——

  1. 读取阻塞,直到第一次设置所有三个映射。
  2. 现在第二次,如果我要更新地图,我仍然可以返回所有三个旧地图值(在所有三个地图上完成更新之前),或者它应该阻止并在更新完成时返回所有新的三个地图值在所有三张地图上。

因为我有三个 Maps - primaryMappingsecondaryMapping所以tertiaryMapping它应该返回三个更新地图的所有新值,或者它应该返回地图的所有旧值。基本上,在更新时我不想返回primaryMapping旧值、secondaryMapping新值和tertiaryMapping新值。

它应该是一致的,要么应该返回旧值,要么应该在更新地图后返回新值。就我而言,地图更新将在三个月或四个月内发生一次。

下面是我ClientData正在使用的课程,ReentrantLock其中有整个逻辑 -

public class ClientData {

    private static final class MapContainer {
        private Map<String, Map<Integer, String>> value = null;

        public Map<String, Map<Integer, String>> getValue() {
            return value;
        }

        public void setValue(Map<String, Map<Integer, String>> value) {
            this.value = value;
        }
    }

    private static final MapContainer primaryMapping = new MapContainer();
    private static final MapContainer secondaryMapping = new MapContainer();
    private static final MapContainer tertiaryMapping = new MapContainer();
    private static final MapContainer[] containers = {primaryMapping, secondaryMapping, tertiaryMapping};
    private static boolean allset = false;
    private static final Lock lock = new ReentrantLock();
    private static final Condition allsetnow = lock.newCondition();

    private static final Map<String, Map<Integer, String>> getMapping(MapContainer container) {
        lock.lock();
        try {
            while (!allset) {
                allsetnow.await();
            }
            return container.getValue();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // reset interruptedd state.
            throw new IllegalStateException(e);
        } finally {
            lock.unlock();
        }

    }

    public static void setAllMappings(Map<String, Map<Integer, String>> primary,
            Map<String, Map<Integer, String>> secondary,
            Map<String, Map<Integer, String>> tertiary) {
        lock.lock();
        try{

            // how  to avoid this?
            if (allset) {
                throw new IllegalStateException("All the maps are already set");
            }

            primaryMapping.setValue(primary);
            secondaryMapping.setValue(secondary);
            tertiaryMapping.setValue(tertiary);
            allset = true;
            allsetnow.signalAll();
        } finally {
            lock.unlock();
        }
    }       


    public static Map<String, Map<Integer, String>> getPrimaryMapping() {
        return getMapping(primaryMapping);
    }

    public static Map<String, Map<Integer, String>> getSecondaryMapping() {
        return getMapping(secondaryMapping);
    }

    public static Map<String, Map<Integer, String>> getTertiaryMapping() {
        return getMapping(tertiaryMapping);
    }       
}

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

public class TempScheduler {

    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        public void startScheduler() {
            final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate(new Runnable() {
                public void run() {
                try {
                    callServers();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                }
            }, 0, 10, TimeUnit.MINUTES);
        }
    }

    // call the servers and get the data and then parse 
    // the response.
    private void callServers() {
        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) {
        //...       
        ConcurrentHashMap<String, Map<Integer, String>> primaryTables = null;
        ConcurrentHashMap<String, Map<Integer, String>> secondaryTables = null;
        ConcurrentHashMap<String, Map<Integer, String>> tertiaryTables = null;

        //...

        // store the data in ClientData class variables if anything has changed  
        // which can be used by other threads
        if(changed) {
            ClientData.setAllMappings(primaryTables, secondaryTables, tertiaryTables);
        }
    }
}

我将在我的主应用程序的主线程中使用,getPrimaryMappinggetSecondaryMapping类,所以我想从这三个映射中返回所有新值集,或者如果发生更新,则阻止它并返回所有新值集更新完成后的那三张地图。getTertiaryMappingClientData

问题陈述:-

在我的代码库中,如上所示ClientData,我想,一旦第一次设置地图,我将无法更新地图,因为这条线会导致问题,它会抛出异常,然后还有如何如上所示实施我的第二点?

// how  to avoid this?
if (allset) {
    throw new IllegalStateException("All the maps are already set");
}

我怎样才能成功地实现以上两点?我想,我在这里遗漏了一些非常小的东西?我想在这里使用 ReentrantLock,但也欢迎任何其他建议。我主要关心的是性能问题。因为我将在三个月内对这三张地图进行一次设置,所以这很好。但是每秒每 1000 个请求就会从主应用程序代码中获取三个地图,所以我想要非常快。

最初,我想删除这个 if 语句 -

// how  to avoid this?
if (allset) {
    throw new IllegalStateException("All the maps are already set");
}

但我怀疑,从那时起,当我更新地图时,线程之间的地图会不匹配?

这就是方式,我正在ClientData class从主应用程序线程中读取值 -

String data1 = ClientData.getPrimaryMapping().get(some_value1).get(some_value2);
String data2 = ClientData.getSecondaryMapping().get(some_value1).get(some_value3);
String data3 = ClientData.getTertiaryMapping().get(some_value1).get(some_value4);

更新:-

CountDownLatch满足上述所有条件的另一种解决方案-

以下是我ClientData正在使用的课程CountDownLatch-

public class ClientData {

    public static class Mappings {
        public final Map<String, Map<Integer, String>> primary;
        public final Map<String, Map<Integer, String>> secondary;
        public final Map<String, Map<Integer, String>> tertiary;

        public Mappings(
            Map<String, Map<Integer, String>> primary,
            Map<String, Map<Integer, String>> secondary,
            Map<String, Map<Integer, String>> tertiary
        ) {
            this.primary = primary;
            this.secondary = secondary;
            this.tertiary = tertiary;
        }
    }

    private static final AtomicReference<Mappings> mappings = new AtomicReference<>();
    private static final CountDownLatch hasBeenInitialized = new CountDownLatch(1);

    public static Mappings getMappings() {
        try {
            hasBeenInitialized.await();
            return mappings.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    }

    public static void setMappings(
        Map<String, Map<Integer, String>> primary,
        Map<String, Map<Integer, String>> secondary,
        Map<String, Map<Integer, String>> tertiary
    ) {
        setMappings(new Mappings(primary, secondary, tertiary));
    }

    public static void setMappings(Mappings newMappings) {
        mappings.set(newMappings);
        hasBeenInitialized.countDown();
    }
}

我将ClientData像这样在主应用程序线程中使用类 -

Mappings mappings = ClientData.getMappings(); 
// use mappings.primary 
// use mappings.secondary 
// use mappings.tertiary

这段代码对性能有影响吗?简而言之,哪一个会更好,我应该使用ReentrantReadWriteLock还是上述CountDownLatch一种解决方案?

所以现在的问题是,CountDownLatch解决方案还是解决ReentrantReadWriteLock方案?哪一个在高读/低写用例中表现更好?

如果可能的话,任何人都可以提供一个ReentrantReadWriteLock基于我的上述解决方案的示例吗?这样,我将能够将性能与CountDownLatch我之前的解决方案进行比较ReentrantReadWriteLock

因为我无法想出一个ReentrantReadWriteLock用于上述用例的解决方案。

笔记:-

就我而言,写入将在三四个月内发生一次。但是读取将以非常高的速度从多个线程发生,1000每秒请求数。所以它必须非常快。

4

4 回答 4

2

您应该按照引入Mappings. 您不需要单独管理三个引用(这是复杂的地方)。相反,管理一个参考。

class ClientData {
  // This is immutable after creation (like your OP)
  class Mappings { /* definition from your original post */ }

  // this should be volatile;
  private static volatile Mappings instance;

  // the read path proceeds without acquiring any lock at all.  Hard to 
  // get faster than a volatile read.  Note the double-checked locking pattern
  // works JDK 6 or greater when using volatile references (above)
  public static Mappings getMappings() {
    Mappings result = instance;
    if(result == null) {
       synchronized(ClientData.class) {
          result = instance;
          // recall while() is required to handle spurious wakeup
          while(result == null) {
             ClientData.class.wait();
             result = instance;
          }
       }
    }
  }

  public static setMappings(Map one, Map two, Map three) {
    synchronized(ClientData.class) {
      instance = new Mappings(one,two,three);
      ClientData.class.notifyAll()
    }
  }
}

我认为这有以下好处:

  1. 读取路径上不需要锁。这是 Mappings 类的不变性的副作用。易失性读取真的很快。
  2. 在 getMappings() 之前输入它的调用者只是等待它被设置。
  3. 无需担心第三方对象语义

有点不幸(恕我直言)java没有内置一个好的“等待参考”。但不能要求一切!第三方库有一些支持——GuavaSuppliers.memoize()是一个很好的起点。

祝你的项目好运。

于 2014-05-07T04:41:56.720 回答
1

你是对的,你需要删除if (allset)块。否则你将无法更新。

因此,看起来您必须解决以下可能出现的问题:

  1. 你打电话ClientData.getPrimaryMapping()
  2. 出现了一个疯狂的更新
  3. 你打电话ClientData.getSecondaryMapping()- 现在你有不匹配的数据

我将创建一个方法来一次获取所有地图,而不是一个接一个地调用所有三个映射方法并在其间进行更新。该方法应返回地图的列表(或数组或其他类型的容器)。通过将锁定移至此方法,您可以确保在获取地图时不会有任何更新。

例如这样的事情:

// create a new list on each update
private static List<Map<String, Map<Integer, String>>> mappings;

public static final List<Map<String, Map<Integer, String>>> getAllMappings() {
    lock.lock();
    try {
        while (!allset) {
            allsetnow.await();
        }
        return mappings;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // reset interruptedd state.
        throw new IllegalStateException(e);
    } finally {
        lock.unlock();
    }

}

我想它也可能会更快,因为更少的方法调用和更少的 try catch 块,但如果你真的关心性能,你应该衡量什么表现最好。

于 2014-04-29T22:14:53.837 回答
1

如果你有这样的代码块

String data1=ClientData.getPrimaryMapping().get(some_value1).get(some_value2);
String data2=ClientData.getSecondaryMapping().get(some_value1).get(some_value3);
String data3=ClientData.getTertiaryMapping().get(some_value1).get(some_value4);

getMapping修复可能的比赛的方法中,您无能为力。这三行之间总是会发生更新,从而导致 , 和 的值不一致,data1无论您在调用的方法中同步的效果如何(一旦您允许更新)。data2data3

解决此问题的唯一方法是将同步逻辑移动到调用者中。在上面的示例代码中,所有三个语句都必须放在同一个保护块中。好消息是这将有助于您的性能,因为如果您做得正确,那么您就不需要在被调用的方法中加锁。因此,您只需对整个区块执行一次即可,而不是锁定和解锁三次。

对于具有大量读取且仅偶尔更新的用例,使用ReadWriteLock允许并发读取是自然的选择。

于 2014-04-30T10:12:39.080 回答
0

听起来像是 ReadWriteLock 的工作。

任何会导致三个映射不一致的方法或过程,都应该在开始前锁定写锁,结束后解锁写锁:

java.util.concurrent.locks.ReaderWriterLock readerWriterLock = ...;

readerWriterLock.writeLock().lock()
try {
    ...update maps...
} finally {
    readerWriterLock.writeLock().unlock()
}

如果涉及多个线程,会稍微复杂一些,但原理保持不变:当您不希望读者访问映射时,保持写锁锁定。

任何需要使用映射中数据的方法都应该锁定读锁。这看起来和我上面的例子一样,但是用“readLock()”代替“writeLock()”。

读取阻塞,直到第一次设置所有三个映射。

简单的!只需确保在启动任何可以读取映射的线程之前锁定 writeLock,并确保在映射设置为其初始一致状态后解锁它。

于 2014-05-06T01:34:55.207 回答