26

I need to implement global object collecting statistics for web server. I have Statistics singleton, which has method addSample(long sample), which subsequently call updateMax. This has to be obviously thread-safe. I have this method for updating maximum of whole Statistics:

AtomicLong max;

private void updateMax(long sample) {
    while (true) {
        long curMax = max.get();
        if (curMax < sample) {
            boolean result = max.compareAndSet(curMax, sample);
            if (result) break;
        } else {
            break;
        }
    }
}

Is this implementation correct? I am using java.util.concurrent, because I believe it would be faster than simple synchronized. Is there some other / better way to implement this?

Tibor Blenessy
  • 4,254
  • 29
  • 35
  • I appreciate Jon Skeet's comment about trying the synchronized version first. I'm not convinced that holding a lock is going to be any more expensive that repeatedly banging on the `volatile Long` that `AtomicLong` holds. – Edward Thomson Jan 13 '12 at 21:09
  • @EdwardThomson AtomicLong.get() is implemented as a volatile read, which on most hardware is basically "free", roughly as expensive as a regular non-volatile memory read. Volatile writing/CAS is the slow part. A single volatile write/CAS in turn are both roughly twice as fast as a synchronized block when contention is low. See e.g. http://mailinator.blogspot.nl/2008/03/how-fast-is-java-volatile-or-atomic-or.html and http://www.evanjones.ca/lmax-disruptor.html – Stefan L Oct 24 '12 at 12:19

5 Answers5

21

As of Java 8, LongAccumulator has been introduced. It is advised as

This class is usually preferable to AtomicLong when multiple threads update a common value that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

You can use it as follows:

LongAccumulator maxId = new LongAccumulator(Long::max, 0); //replace 0 with desired initial value
maxId.accumulate(newValue); //from each thread
lorenzop
  • 525
  • 4
  • 19
13

I think it's correct, but I'd probably rewrite it a little for clarity, and definitely add comments:

private void updateMax(long sample) {
    while (true) {
        long curMax = max.get();
        if (curMax >= sample) {
            // Current max is higher, so whatever other threads are
            // doing, our current sample can't change max.
            break;
        }

        // Try updating the max value, but only if it's equal to the
        // one we've just seen. We don't want to overwrite a potentially
        // higher value which has been set since our "get" call.
        boolean setSuccessful = max.compareAndSet(curMax, sample);

        if (setSuccessful) {
            // We managed to update the max value; no other threads
            // got in there first. We're definitely done.
            break;
        }

        // Another thread updated the max value between our get and
        // compareAndSet calls. Our sample can still be higher than the
        // new value though - go round and try again.
    }
}

EDIT: Usually I'd at least try the synchronized version first, and only go for this sort of lock-free code when I'd found that it was causing a problem.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • @Jon: Whenever I see something like this, I'm always concerned about the looping. I feel like yes, it will work, but I feel like it opens the door for a bunch of threads to come in here at once and all end up looping continuously because the value is constantly being updated. How justified is this concern? – Erick Robertson May 20 '11 at 12:59
  • @Erick: They won't be able to loop *continuously*. Each iteration, either *this* thread quits, or a *different* one will - because we only continue the loop if the value has been updated, which means a different thread has successfully updated it. – Jon Skeet May 20 '11 at 13:13
  • @Jon Thanks for great answer, your code is indeed clearer and comments are perfect – Tibor Blenessy May 20 '11 at 13:14
  • @Erick: Just to ease your pain: in this example, it is clear that the value can only go up. Say Alice calls `updateMax(5)`, Bob calls `updateMax(3)` and Charlie calls `updateMax(1000000)`. The max value will only be update a maximum of three times - once per thread. The order matters very little. Either it will be updated to 3, then to 5, then to 1000000, or it will be updated to 3 and then to 1000000 or to 5 and then to 1000000 or it will only be updated once to 1000000. Remember - once curMax >= sample, the current thread will be finished. – configurator May 20 '11 at 13:54
  • I just found out about [codereview stack exchange](http://codereview.stackexchange.com) maybe this question should be moved there? – Tibor Blenessy May 20 '11 at 15:17
  • @saberduck: Maybe - not sure. I don't hang out there, I'm afraid, so I don't really have a feel for the border. – Jon Skeet May 20 '11 at 15:20
  • This still makes me nervous. I understand in principle why it works. I guess I just feel like this approach loses a bit of control of thread management. I'm not saying it's not the best solution, but I'd like to get to a place where I'm comfortable with it. – Erick Robertson May 24 '11 at 12:00
  • Now it seems that Java 8 provides ready-made solution for this problem http://download.java.net/jdk8/docs/api/java/util/concurrent/atomic/LongAccumulator.html – Tibor Blenessy Mar 18 '14 at 15:46
  • @saberduck: All kinds of things are going to be simpler with a more functional emphasis on Java :) – Jon Skeet Mar 18 '14 at 15:49
  • @JonSkeet I know it's been a while but if you declare the `setSuccessful` outside the loop and change the loop condition to `while (!setSuccessful)`, IMHO, it expresses the intent better than `while (true)` and you can eliminate the conditional `break` as well. p.s: Conventionally `setSuccessful` should be `isSuccessful` :) – Abhijit Sarkar Jan 05 '15 at 16:32
7

With Java 8 you can take advantage of functional interfaces and a simple lamda expression to solve this with one line and no looping:

private void updateMax(long sample) {
    max.updateAndGet(curMax -> (sample > curMax) ? sample : curMax);
}

The solution uses the updateAndGet(LongUnaryOperator) method. The current value is contained in curMax and using the conditional operator a simple test is performed replacing the current max value with the sample value if the sample value is greater than the current max value.

dur
  • 15,689
  • 25
  • 79
  • 125
3

as if you didn't have your pick of answers, here's mine:

// while the update appears bigger than the atomic, try to update the atomic.
private void max(AtomicDouble atomicDouble, double update) {
    double expect = atomicDouble.get();
    while (update > expect) {
        atomicDouble.weakCompareAndSet(expect, update);
        expect = atomicDouble.get();
    }
}

it's more or less the same as the accepted answer, but doesn't use break or while(true) which I personally don't like.

EDIT: just discovered DoubleAccumulator in java 8. the documentation even says this is for summary statistics problems like yours:

DoubleAccumulator max = new DoubleAccumulator(Double::max, Double.NEGATIVE_INFINITY);
parallelStream.forEach(max::accumulate);
max.get();
Jayen
  • 5,653
  • 2
  • 44
  • 65
2

I believe what you did is correct, but this is a simpler version that I also think is correct.

private void updateMax(long sample){
      //this takes care of the case where between the comparison and update steps, another thread updates the max

      //For example:
      //if the max value is set to a higher max value than the current value in between the comparison and update step
      //sample will be the higher value from the other thread
      //this means that the sample will now be higher than the current highest (as we just set it to the value passed into this function)
      //on the next iteration of the while loop, we will update max to match the true max value
      //we will then fail the while loop check, and be done with trying to update.
      while(sample > max.get()){
          sample = max.getAndSet(sample);  
      }
}
Jeff Patti
  • 37
  • 3
  • 5
    This could temporarily downgrade the maximum: if sample is 6 max.get() returns 5, then somebody updates to 7, you will set 6 (returning 7). The next while loop will see 7 > 6 and set it (as expected). Result is that max will be 5 7 6 7. – eckes Sep 20 '14 at 06:06
  • 1
    yes, please remove "sample =". From java doc of getAndSet method "Atomically sets to the given value and returns the old value." – demon101 May 27 '15 at 12:08
  • @eckes is correct, but there's no guarantee that the order will be as he/she says. it could be `6>5`, `7>5`, `sample=7`, `7>7`, `sample=6`, `6>6`, and at the end, `sample` will be 6 and not 7... why is 6 afraid of 7? because 7 8 9. no more! 6 just ate 7! – Jayen Feb 19 '18 at 04:15