java concurrency: many writers, one reader

前端 未结 9 2089
我寻月下人不归
我寻月下人不归 2021-01-30 18:22

I need to gather some statistics in my software and i am trying to make it fast and correct, which is not easy (for me!)

first my code so far with two classes, a StatsS

相关标签:
9条回答
  • 2021-01-30 19:11

    A different approach to the problem is to exploit the (trivial) thread safety via thread confinement. Basically create a single background thread that takes care of both reading and writing. It has a pretty good characteristics in terms of scalability and simplicity.

    The idea is that instead of all the threads trying to update the data directly, they produce an "update" task for the background thread to process. The same thread can also do the read task, assuming some lags in processing updates is tolerable.

    This design is pretty nice because the threads will no longer have to compete for a lock to update data, and since the map is confined to a single thread you can simply use a plain HashMap to do get/put, etc. In terms of implementation, it would mean creating a single threaded executor, and submitting write tasks which may also perform the optional "collectAndSave" operation.

    A sketch of code may look like the following:

    public class StatsService {
        private ExecutorService executor = Executors.newSingleThreadExecutor();
        private final Map<String,Long> stats = new HashMap<String,Long>();
    
        public void notify(final String key) {
            Runnable r = new Runnable() {
                public void run() {
                    Long value = stats.get(key);
                    if (value == null) {
                        value = 1L;
                    } else {
                        value++;
                    }
                    stats.put(key, value);
                    // do the optional collectAndSave periodically
                    if (timeToDoCollectAndSave()) {
                        collectAndSave();
                    }
                }
            };
            executor.execute(r);
        }
    }
    

    There is a BlockingQueue associated with an executor, and each thread that produces a task for the StatsService uses the BlockingQueue. The key point is this: the locking duration for this operation should be much shorter than the locking duration in the original code, so the contention should be much less. Overall it should result in a much better throughput and latency.

    Another benefit is that since only one thread reads and writes to the map, plain HashMap and primitive long type can be used (no ConcurrentHashMap or atomic types involved). This also simplifies the code that actually processes it a great deal.

    Hope it helps.

    0 讨论(0)
  • 2021-01-30 19:12

    Why don't you use java.util.concurrent.ConcurrentHashMap<K, V>? It handles everything internally avoiding useless locks on the map and saving you a lot of work: you won't have to care about synchronizations on get and put..

    From the documentation:

    A hash table supporting full concurrency of retrievals and adjustable expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access.

    You can specify its concurrency level:

    The allowed concurrency among update operations is guided by the optional concurrencyLevel constructor argument (default 16), which is used as a hint for internal sizing. The table is internally partitioned to try to permit the indicated number of concurrent updates without contention. Because placement in hash tables is essentially random, the actual concurrency will vary. Ideally, you should choose a value to accommodate as many threads as will ever concurrently modify the table. Using a significantly higher value than you need can waste space and time, and a significantly lower value can lead to thread contention. But overestimates and underestimates within an order of magnitude do not usually have much noticeable impact. A value of one is appropriate when it is known that only one thread will modify and all others will only read. Also, resizing this or any other kind of hash table is a relatively slow operation, so, when possible, it is a good idea to provide estimates of expected table sizes in constructors.

    As suggested in comments read carefully the documentation of ConcurrentHashMap, especially when it states about atomic or not atomic operations.

    To have the guarantee of atomicity you should consider which operations are atomic, from ConcurrentMap interface you will know that:

    V putIfAbsent(K key, V value)
    V replace(K key, V value)
    boolean replace(K key,V oldValue, V newValue)
    boolean remove(Object key, Object value)
    

    can be used safely.

    0 讨论(0)
  • 2021-01-30 19:13

    If we ignore the harvesting part and focus on the writing, the main bottleneck of the program is that the stats are locked at a very coarse level of granularity. If two threads want to update different keys, they must wait.

    If you know the set of keys in advance, and can preinitialize the map so that by the time an update thread arrives the key is guaranteed to exist, you would be able to do locking on the accumulator variable instead of the whole map, or use a thread-safe accumulator object.

    Instead of implementing this yourself, there are map implementations that are designed specifically for concurrency and do this more fine-grained locking for you.

    One caveat though are the stats, since you would need to get locks on all the accumulators at roughly the same time. If you use an existing concurrency-friendly map, there might be a construct for getting a snapshot.

    0 讨论(0)
提交回复
热议问题