Skip to content

Commit 6df5495

Browse files
Lyin-OTTFaherbert
authored andcommitted
Synchronize getPercentile of SynchronizedDescriptiveStatistics
DescriptiveStatistics.getPercentile sets the quantile on the shared percentile implementation and then calls apply, which is synchronized in SynchronizedDescriptiveStatistics. The setQuantile step ran outside the instance lock, so a concurrent getPercentile call could swap the quantile in between the two steps and a caller could end up computing a percentile for a quantile it never requested. Override getPercentile as synchronized so the quantile update and the apply are atomic with respect to the other synchronized methods on the same instance. Add a reflection test guarding the synchronized modifier and a concurrency test that holds the instance monitor while another thread requests a different quantile, asserting the requested quantile is the one applied. Signed-off-by: Lyin-OTTF <htao08203@gmail.com>
1 parent 50ba718 commit 6df5495

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

‎commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/descriptive/SynchronizedDescriptiveStatistics.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ public synchronized double apply(UnivariateStatistic stat) {
9494
return super.apply(stat);
9595
}
9696

97+
/**
98+
* {@inheritDoc}
99+
*/
100+
@Override
101+
public synchronized double getPercentile(double p) throws MathIllegalStateException, MathIllegalArgumentException {
102+
return super.getPercentile(p);
103+
}
104+
97105
/**
98106
* {@inheritDoc}
99107
*/

‎commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/descriptive/SynchronizedDescriptiveStatisticsTest.java‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.lang.reflect.Modifier;
2121
import java.util.concurrent.CountDownLatch;
2222
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicReference;
2324

2425
import org.junit.Assert;
2526
import org.junit.Test;
@@ -56,6 +57,17 @@ public void testMutatorsAreSynchronized() throws NoSuchMethodException {
5657
}
5758
}
5859

60+
@Test
61+
public void testGetPercentileIsSynchronized() throws NoSuchMethodException {
62+
// DescriptiveStatistics.getPercentile sets the shared percentile quantile before calling the
63+
// synchronized apply, so the whole method (setQuantile + apply) has to take the instance lock;
64+
// otherwise a concurrent call can swap the quantile in between the two steps.
65+
final Method getPercentile = SynchronizedDescriptiveStatistics.class.getDeclaredMethod(
66+
"getPercentile", double.class);
67+
Assert.assertTrue("getPercentile(double) must be synchronized",
68+
Modifier.isSynchronized(getPercentile.getModifiers()));
69+
}
70+
5971
@Test
6072
public void testRemoveMostRecentValueWaitsForInstanceLock() throws InterruptedException {
6173
assertWaitsForInstanceLock(false);
@@ -101,4 +113,40 @@ private void assertWaitsForInstanceLock(final boolean replace) throws Interrupte
101113
Assert.assertEquals(0, stats.getN());
102114
}
103115
}
116+
117+
@Test
118+
public void testGetPercentileAppliesRequestedQuantileAtomically() throws InterruptedException {
119+
final SynchronizedDescriptiveStatistics stats = new SynchronizedDescriptiveStatistics();
120+
final DescriptiveStatistics reference = new DescriptiveStatistics();
121+
for (int i = 1; i <= 100; i++) {
122+
stats.addValue(i);
123+
reference.addValue(i);
124+
}
125+
final double expectedQuartile = reference.getPercentile(25);
126+
final double expectedTop = reference.getPercentile(90);
127+
Assert.assertTrue("test setup: the two quantiles must differ", expectedQuartile != expectedTop);
128+
129+
final AtomicReference<Double> other = new AtomicReference<>();
130+
final Thread percentileCaller = new Thread(() -> other.set(stats.getPercentile(25)), "percentile-caller");
131+
synchronized (stats) {
132+
percentileCaller.start();
133+
// Hold the instance monitor and wait until the other thread blocks trying to acquire it.
134+
// If getPercentile were not synchronized, the other thread would already have set the
135+
// quantile to 25 outside the lock and be parked inside apply(); with the synchronized
136+
// override it parks at the method entry, so our own call cannot disturb its quantile.
137+
final long deadline = System.currentTimeMillis() + 5000;
138+
while (percentileCaller.getState() != Thread.State.BLOCKED
139+
&& System.currentTimeMillis() < deadline) {
140+
Thread.sleep(1);
141+
}
142+
Assert.assertEquals("other thread should be blocked on the instance monitor",
143+
Thread.State.BLOCKED, percentileCaller.getState());
144+
Assert.assertEquals("our own call must still see the requested quantile",
145+
expectedTop, stats.getPercentile(90), 0);
146+
}
147+
percentileCaller.join(5000);
148+
Assert.assertFalse("other thread did not complete", percentileCaller.isAlive());
149+
Assert.assertEquals("getPercentile must apply the requested quantile atomically",
150+
expectedQuartile, other.get(), 0);
151+
}
104152
}

0 commit comments

Comments
 (0)