├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ ├── dev │ └── mccue │ │ └── left_right │ │ ├── LeftRight.java │ │ └── LeftRightMap.java │ └── module-info.java └── test └── java └── LeftRightMapTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright [2020] [Ethan McCue] 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/bowbahdoe/leftright-map-java.svg)](https://jitpack.io/#bowbahdoe/leftright-map-java) 2 | 3 | ## A (hopefully) Fast, (hopefully) Thread safe map inspired by evmap 4 | 5 | ### Maven 6 | ```xml 7 | 8 | 9 | jitpack.io 10 | https://jitpack.io 11 | 12 | 13 | ``` 14 | ```xml 15 | 16 | com.github.bowbahdoe 17 | leftright-map-java 18 | 0.1.3 19 | 20 | ``` 21 | ### Gradle 22 | ```groovy 23 | allprojects { 24 | repositories { 25 | ... 26 | maven { url 'https://jitpack.io' } 27 | } 28 | } 29 | ``` 30 | 31 | ```groovy 32 | dependencies { 33 | implementation 'com.github.bowbahdoe:leftright-map-java:0.1.3' 34 | } 35 | ``` 36 | 37 | ### Leiningen 38 | ```clojure 39 | :repositories [["jitpack" "https://jitpack.io"]] 40 | :dependencies [[com.github.bowbahdoe/leftright-map-java "0.1.3"]] 41 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | dev.mccue 8 | left_right 9 | 0.1.3 10 | jar 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | 18 | junit 19 | junit 20 | 4.12 21 | test 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-compiler-plugin 30 | 3.8.1 31 | 32 | 11 33 | 11 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-source-plugin 40 | 3.0.1 41 | 42 | 43 | attach-sources 44 | 45 | jar 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-javadoc-plugin 54 | 3.2.0 55 | 56 | 57 | attach-javadocs 58 | 59 | jar 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/java/dev/mccue/left_right/LeftRight.java: -------------------------------------------------------------------------------- 1 | package dev.mccue.left_right; 2 | 3 | import java.util.ArrayList; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | import java.util.concurrent.atomic.AtomicReference; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.function.Predicate; 9 | import java.util.function.Supplier; 10 | import java.util.function.ToIntFunction; 11 | 12 | /** 13 | * A Left-Right Concurrency primitive. 14 | * 15 | * https://www.youtube.com/watch?v=eLNAMEoKAAc 16 | */ 17 | final class LeftRight { 18 | private final ReaderFactory readerFactory; 19 | private final Writer writer; 20 | 21 | private LeftRight(ReaderFactory readerFactory, Writer writer) { 22 | this.readerFactory = readerFactory; 23 | this.writer = writer; 24 | } 25 | 26 | /** 27 | * @return A thread safe factory for producing readers. 28 | */ 29 | ReaderFactory readerFactory() { 30 | return this.readerFactory; 31 | } 32 | 33 | /** 34 | * @return The writer for the map. 35 | */ 36 | Writer writer() { 37 | return this.writer; 38 | } 39 | 40 | /** 41 | * Creates a writer and factory for readers.The reader factory can be asked 42 | * for any number of readers on any thread. The Writer is not thread safe and 43 | * must be owned by a single thread or otherwise coordinated. 44 | */ 45 | static LeftRight create(Supplier createDS) { 46 | final var readers = new ArrayList>(); 47 | final var readerDS = createDS.get(); 48 | final var readerDSRef = new AtomicReference<>(readerDS); 49 | final var writerDS = createDS.get(); 50 | 51 | final var readerFactory = new ReaderFactory<>( 52 | readers, 53 | readerDSRef 54 | ); 55 | 56 | final var writer = new Writer<>( 57 | readers, 58 | readerDS, 59 | readerDSRef, 60 | writerDS 61 | ); 62 | 63 | return new LeftRight<>(readerFactory, writer); 64 | } 65 | 66 | 67 | /** 68 | * Creates a reader to the underlying Data structure. This operation should be 69 | * totally threadsafe and efficient to do from any thread. 70 | */ 71 | static final class ReaderFactory { 72 | private final ArrayList> readers; 73 | private final AtomicReference dsRef; 74 | 75 | private ReaderFactory(ArrayList> readers, 76 | AtomicReference dsRef) { 77 | this.readers = readers; 78 | this.dsRef = dsRef; 79 | } 80 | 81 | /** 82 | * @return A new reader. This Reader is **not** thread-safe. For each thread that wants to read, 83 | * they should create their own readers with this factory or synchronize usage some other way. 84 | */ 85 | Reader createReader() { 86 | final var reader = new Reader<>(this.dsRef); 87 | synchronized (this.readers) { 88 | this.readers.add(reader); 89 | } 90 | return reader; 91 | } 92 | } 93 | 94 | /** 95 | * A Reader to the Data Structure. Each reader must have only a single owner and is not 96 | * thread safe. 97 | */ 98 | static final class Reader { 99 | private final AtomicReference dsRef; 100 | private volatile int epoch; 101 | 102 | private Reader(AtomicReference dsRef) { 103 | this.epoch = 0; 104 | this.dsRef = dsRef; 105 | } 106 | 107 | private int epoch() { 108 | return this.epoch; 109 | } 110 | 111 | void incrementEpoch() { 112 | this.epoch++; 113 | } 114 | 115 | DS currentDsState() { 116 | return this.dsRef.get(); 117 | } 118 | 119 | T read(Function readOperation) { 120 | this.incrementEpoch(); 121 | final var currentDS = dsRef.get(); 122 | try { 123 | return readOperation.apply(currentDS); 124 | } 125 | finally { 126 | this.incrementEpoch(); 127 | } 128 | } 129 | 130 | int readInt(ToIntFunction readOperation) { 131 | this.incrementEpoch(); 132 | final var currentDS = dsRef.get(); 133 | try { 134 | return readOperation.applyAsInt(currentDS); 135 | } 136 | finally { 137 | this.incrementEpoch(); 138 | } 139 | } 140 | 141 | boolean readBool(Predicate readOperation) { 142 | this.incrementEpoch(); 143 | final var currentDS = dsRef.get(); 144 | try { 145 | return readOperation.test(currentDS); 146 | } 147 | finally { 148 | this.incrementEpoch(); 149 | } 150 | } 151 | 152 | void readVoid(Consumer readOperation) { 153 | this.incrementEpoch(); 154 | final var currentDS = dsRef.get(); 155 | try { 156 | readOperation.accept(currentDS); 157 | } 158 | finally { 159 | this.incrementEpoch(); 160 | } 161 | } 162 | } 163 | 164 | 165 | 166 | /** 167 | * Represents a loggable and repeatable operation on a data structure. 168 | * @param The Data Structure the Operation works on. 169 | */ 170 | interface Operation { 171 | R perform(DS ds); 172 | } 173 | 174 | static final class Writer { 175 | /** 176 | * The log of operations performed on this data structure in the current refresh cycle. 177 | */ 178 | private final ArrayList> opLog; 179 | 180 | /** 181 | * A list of all of the epoch counts for the readers. 182 | */ 183 | private final ArrayList> readers; 184 | 185 | /** 186 | * The data structure that readers should eventually be reading from. 187 | */ 188 | private DS readerDS; 189 | 190 | /** 191 | * The reference that readers should be looking at to pick the data structure to read from. 192 | */ 193 | private final AtomicReference readerDSRef; 194 | 195 | /** 196 | * The data structure that the writer is currently reading from. 197 | */ 198 | private DS writerDS; 199 | 200 | private Writer(ArrayList> readers, 201 | DS readerDS, 202 | AtomicReference readerDSRef, 203 | DS writerDS) { 204 | this.opLog = new ArrayList<>(); 205 | this.readers = readers; 206 | this.readerDS = readerDS; 207 | this.readerDSRef = readerDSRef; 208 | this.writerDS = writerDS; 209 | } 210 | 211 | R write(Operation operation) { 212 | R res = operation.perform(this.writerDS); 213 | this.opLog.add(operation); 214 | return res; 215 | } 216 | 217 | T read(Function readOperation) { 218 | return readOperation.apply(this.writerDS); 219 | } 220 | 221 | int readInt(ToIntFunction readOperation) { 222 | return readOperation.applyAsInt(this.writerDS); 223 | } 224 | 225 | boolean readBool(Predicate readOperation) { 226 | return readOperation.test(this.writerDS); 227 | } 228 | 229 | void readVoid(Consumer readOperation) { 230 | readOperation.accept(this.writerDS); 231 | } 232 | 233 | /** 234 | * Propagates writes to readers. 235 | */ 236 | void refresh() { 237 | // Swap the pointer for the readers 238 | this.readerDSRef.set(this.writerDS); 239 | final var pivot = this.writerDS; 240 | this.writerDS = this.readerDS; 241 | this.readerDS = pivot; 242 | 243 | // Track the last epoch we read from the readers. 244 | final class StillReadingReader { 245 | private final Reader reader; 246 | private final int previousEpoch; 247 | 248 | StillReadingReader(Reader reader, int previousEpoch) { 249 | this.previousEpoch = previousEpoch; 250 | this.reader = reader; 251 | } 252 | 253 | boolean hasMovedOn() { 254 | final var newEpoch = this.reader.epoch(); 255 | return newEpoch != this.previousEpoch; 256 | } 257 | } 258 | 259 | // Make sure readers have moved on 260 | synchronized (this.readers) { // No new readers while we are refreshing. 261 | var readers = this.readers; 262 | var stillReading = new ArrayList(); 263 | for (final var reader : readers) { 264 | final var epochValue = reader.epoch(); 265 | if (epochValue % 2 == 1) { 266 | stillReading.add(new StillReadingReader(reader, epochValue)); 267 | } 268 | } 269 | 270 | while (stillReading.size() != 0) { 271 | final var needToRetry = new ArrayList(); 272 | for (final var reader : stillReading) { 273 | if (!reader.hasMovedOn()) { 274 | needToRetry.add(reader); 275 | } 276 | } 277 | stillReading = needToRetry; 278 | } 279 | } 280 | 281 | // Apply operations to new data structure 282 | for (final var operation : this.opLog) { 283 | operation.perform(this.writerDS); 284 | } 285 | 286 | // Clear operation log 287 | this.opLog.clear(); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/dev/mccue/left_right/LeftRightMap.java: -------------------------------------------------------------------------------- 1 | package dev.mccue.left_right; 2 | 3 | import java.io.Closeable; 4 | import java.util.HashMap; 5 | import java.util.function.BiConsumer; 6 | 7 | /** 8 | * A (hopefully) Fast, (hopefully) Thread safe Map data structure inspired by Jon Gjengset's evmap 9 | * rust crate and talks on Youtube. Design is hypothetically optimized for lots of 10 | * concurrent reads. 11 | * 12 | * Neither Readers or Writers implement java.util.Map at this stage, so if that is 13 | * a showstopper for you then 🤷. 14 | * 15 | * @param The Key, assumed to be a valid immutable HashMap key. 16 | * @param The Value, assumed to be safe to share across threads. 17 | */ 18 | public final class LeftRightMap { 19 | private final ReaderFactory readerFactory; 20 | private final ThreadSafeReader threadSafeReader; 21 | private final Writer writer; 22 | 23 | private LeftRightMap(ReaderFactory readerFactory, Writer writer) { 24 | this.readerFactory = readerFactory; 25 | this.threadSafeReader = new ThreadSafeReader<>(readerFactory); 26 | this.writer = writer; 27 | } 28 | 29 | /** 30 | * @return A thread safe factory for producing readers. 31 | */ 32 | public ReaderFactory readerFactory() { 33 | return this.readerFactory; 34 | } 35 | 36 | /** 37 | * @return A thread safe reader into the map. Uses thread locals to give each physical thread its own reader. 38 | */ 39 | public ThreadSafeReader threadSafeReader() { 40 | return this.threadSafeReader; 41 | } 42 | 43 | /** 44 | * @return The writer for the map. Should only be used by a single thread. 45 | */ 46 | public Writer writer() { 47 | return this.writer; 48 | } 49 | 50 | /** 51 | * Creates a writer and factory for readers.The reader factory can be asked 52 | * for any number of readers on any thread. The Writer is not thread safe and 53 | * must be owned by a single thread or otherwise coordinated. 54 | * 55 | * @param The Key, assumed to be a valid immutable HashMap key. 56 | * @param The Value, assumed to be safe to share across threads. 57 | */ 58 | public static LeftRightMap create() { 59 | final var leftRight = LeftRight.>create(HashMap::new); 60 | final var readerFactory = new ReaderFactory<>(leftRight.readerFactory()); 61 | final var writer = new Writer<>(leftRight.writer()); 62 | return new LeftRightMap<>(readerFactory, writer); 63 | } 64 | 65 | /** 66 | * A Thread Safe factory for creating Readers. 67 | */ 68 | public static final class ReaderFactory { 69 | private final LeftRight.ReaderFactory> innerFactory; 70 | 71 | 72 | private ReaderFactory(LeftRight.ReaderFactory> innerFactory) { 73 | this.innerFactory = innerFactory; 74 | } 75 | 76 | /** 77 | * @return A Reader into the Map. This should be safe to call from any thread, but 78 | * calling it over and over again could degrade performance, since currently Writers 79 | * don't have a ability to "lose" readers. 80 | */ 81 | public Reader createReader() { 82 | return new Reader<>(this.innerFactory.createReader()); 83 | } 84 | } 85 | 86 | /** 87 | * A Reader into the map. The operations of this class are not by themselves thread safe, so each 88 | * reader should only be accessed from a single thread at a time or be coordinated by some other 89 | * mechanism. 90 | */ 91 | public static final class Reader { 92 | private final LeftRight.Reader> innerReader; 93 | 94 | private Reader(LeftRight.Reader> innerReader) { 95 | this.innerReader = innerReader; 96 | } 97 | 98 | public V get(K key) { 99 | this.innerReader.incrementEpoch(); 100 | try { 101 | return this.innerReader.currentDsState().get(key); 102 | } 103 | finally { 104 | this.innerReader.incrementEpoch(); 105 | } 106 | } 107 | 108 | public V getOrDefault(K key, V defaultValue) { 109 | return this.innerReader.read(map -> map.getOrDefault(key, defaultValue)); 110 | } 111 | 112 | public boolean containsKey(K key) { 113 | return this.innerReader.readBool(map -> map.containsKey(key)); 114 | } 115 | 116 | public void forEach(BiConsumer action) { 117 | this.innerReader.readVoid(map -> map.forEach(action)); 118 | } 119 | 120 | public int size() { 121 | return this.innerReader.readInt(HashMap::size); 122 | } 123 | 124 | public boolean isEmpty() { 125 | return this.innerReader.readBool(HashMap::isEmpty); 126 | } 127 | 128 | public boolean containsValue(V value) { 129 | return this.innerReader.readBool(map -> map.containsValue(value)); 130 | } 131 | } 132 | 133 | /** 134 | * A Thread Safe reader into the Map. 135 | * 136 | *

All the methods on this class should be safe to call from any Thread. 137 | * Uses {@link ThreadLocal} to give each thread its own Reader. 138 | */ 139 | public static final class ThreadSafeReader { 140 | private final ThreadLocal> localReader; 141 | 142 | private ThreadSafeReader(ReaderFactory innerFactory) { 143 | this.localReader = ThreadLocal.withInitial(innerFactory::createReader); 144 | } 145 | 146 | public V get(K key) { 147 | return this.localReader.get().get(key); 148 | } 149 | 150 | public V getOrDefault(K key, V defaultValue) { 151 | return this.localReader.get().getOrDefault(key, defaultValue); 152 | } 153 | 154 | public boolean containsKey(K key) { 155 | return this.localReader.get().containsKey(key); 156 | } 157 | 158 | public void forEach(BiConsumer action) { 159 | this.localReader.get().forEach(action); 160 | } 161 | 162 | public int size() { 163 | return this.localReader.get().size(); 164 | } 165 | 166 | public boolean isEmpty() { 167 | return this.localReader.get().isEmpty(); 168 | } 169 | 170 | public boolean containsValue(V value) { 171 | return this.localReader.get().containsValue(value); 172 | } 173 | } 174 | 175 | 176 | /** 177 | * Insert a value into the map. 178 | */ 179 | static final class Put implements LeftRight.Operation, V> { 180 | private final K key; 181 | private final V value; 182 | 183 | public Put(K key, V value) { 184 | this.key = key; 185 | this.value = value; 186 | } 187 | 188 | @Override 189 | public V perform(HashMap map) { 190 | return map.put(key, value); 191 | } 192 | } 193 | 194 | /** 195 | * Insert a value into the map if its not already there. 196 | */ 197 | static final class PutIfAbsent implements LeftRight.Operation, V> { 198 | private final K key; 199 | private final V value; 200 | 201 | public PutIfAbsent(K key, V value) { 202 | this.key = key; 203 | this.value = value; 204 | } 205 | 206 | @Override 207 | public V perform(HashMap map) { 208 | return map.putIfAbsent(key, value); 209 | } 210 | } 211 | 212 | /** 213 | * Remove some key from the map. 214 | */ 215 | static final class Remove implements LeftRight.Operation, V> { 216 | private final K key; 217 | 218 | public Remove(K key) { 219 | this.key = key; 220 | } 221 | 222 | @Override 223 | public V perform(HashMap map) { 224 | return map.remove(key); 225 | } 226 | } 227 | 228 | /** 229 | * Remove some key from the map if it has the matching value. 230 | */ 231 | static final class RemoveWithValue implements LeftRight.Operation, Boolean> { 232 | private final K key; 233 | private final V value; 234 | 235 | public RemoveWithValue(K key, V value) { 236 | this.key = key; 237 | this.value = value; 238 | } 239 | 240 | @Override 241 | public Boolean perform(HashMap map) { 242 | return map.remove(key, value); 243 | } 244 | } 245 | 246 | /** 247 | * Clears all entries from the map. 248 | */ 249 | static final class Clear implements LeftRight.Operation, Void> { 250 | private static final Clear INSTANCE = new Clear<>(); 251 | 252 | private Clear() {} 253 | 254 | @SuppressWarnings("unchecked") 255 | public static Clear getInstance() { 256 | return (Clear) INSTANCE; 257 | } 258 | 259 | @Override 260 | public Void perform(HashMap map) { 261 | map.clear(); 262 | return null; 263 | } 264 | } 265 | 266 | 267 | /** 268 | * A Writer into the Map. 269 | * 270 | *

This is not thread safe, so either a single thread needs to have ownership of the writer 271 | * or access to the writer needs to be coordinated via some other mechanism. 272 | * 273 | *

All writes done are only propagated to readers when {@link Writer#refresh()} 274 | * or {@link Writer#close()} are called. 275 | * 276 | *

Any reads done via the writer will by definition always get the most up to date state of the map. 277 | */ 278 | public static final class Writer implements Closeable { 279 | private final LeftRight.Writer> innerWriter; 280 | 281 | private Writer(LeftRight.Writer> innerWriter) { 282 | this.innerWriter = innerWriter; 283 | } 284 | 285 | public V put(K key, V value) { 286 | return this.innerWriter.write(new Put<>(key, value)); 287 | } 288 | 289 | public V putIfAbsent(K key, V value) { 290 | return this.innerWriter.write(new PutIfAbsent<>(key, value)); 291 | } 292 | 293 | public V remove(K key) { 294 | return this.innerWriter.write(new Remove<>(key)); 295 | } 296 | 297 | public boolean remove(K key, V value) { 298 | return this.innerWriter.write(new RemoveWithValue<>(key, value)); 299 | } 300 | 301 | public void clear() { 302 | this.innerWriter.write(Clear.getInstance()); 303 | } 304 | 305 | public int size() { 306 | return this.innerWriter.readInt(HashMap::size); 307 | } 308 | 309 | public boolean isEmpty() { 310 | return this.innerWriter.readBool(HashMap::isEmpty); 311 | } 312 | 313 | public boolean containsValue(V value) { 314 | return this.innerWriter.readBool(map -> map.containsValue(value)); 315 | } 316 | 317 | public V get(K key) { 318 | return this.innerWriter.read(map -> map.get(key)); 319 | } 320 | 321 | public V getOrDefault(K key, V defaultValue) { 322 | return this.innerWriter.read(map -> map.getOrDefault(key, defaultValue)); 323 | } 324 | 325 | public boolean containsKey(K key) { 326 | return this.innerWriter.readBool(map -> map.containsKey(key)); 327 | } 328 | 329 | public void forEach(BiConsumer action) { 330 | this.innerWriter.readVoid(map -> map.forEach(action)); 331 | } 332 | 333 | /** 334 | * Propagates writes to readers. 335 | */ 336 | public void refresh() { 337 | this.innerWriter.refresh(); 338 | } 339 | 340 | /** 341 | * A close() implementation that calls refresh() for convenient use with try-with-resources. 342 | * 343 | *

344 |          * @code
345 |          * {
346 |          * final var map = LeftRightMap.create();
347 |          * try (final var writer = map.writer()) { // Writes will be propagated at the end of scope.
348 |          *     int key = 0;
349 |          *     if (writer.containsKey(1)) {
350 |          *         writer.put(writer.get(1) + 1);
351 |          *     }
352 |          *     else {
353 |          *         writer.put(1, 0);
354 |          *     }
355 |          * }
356 |          * 
357 | */ 358 | @Override 359 | public void close() { 360 | this.refresh(); 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.mccue.left_right { 2 | exports dev.mccue.left_right; 3 | } -------------------------------------------------------------------------------- /src/test/java/LeftRightMapTests.java: -------------------------------------------------------------------------------- 1 | import dev.mccue.left_right.LeftRightMap; 2 | import java.util.ArrayList; 3 | import java.util.List; 4 | import java.util.Set; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.Future; 7 | import java.util.stream.Collectors; 8 | import org.junit.Test; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertNull; 12 | 13 | 14 | public class LeftRightMapTests { 15 | @Test 16 | public void writesOnlyPropagateOnRefresh() { 17 | final var map = LeftRightMap.create(); 18 | final var reader = map.threadSafeReader(); 19 | final var writer = map.writer(); 20 | 21 | assertNull(reader.get("a")); 22 | writer.put("a", "b"); 23 | assertNull(reader.get("a")); 24 | writer.refresh(); 25 | assertEquals(reader.get("a"), "b"); 26 | } 27 | 28 | @Test 29 | public void tryWithResourcesWillRefresh() { 30 | final var map = LeftRightMap.create(); 31 | final var reader = map.threadSafeReader(); 32 | 33 | try (final var writer = map.writer()) { 34 | writer.put("a", "b"); 35 | assertNull(reader.get("a")); 36 | } 37 | assertEquals(reader.get("a"), "b"); 38 | } 39 | 40 | @Test 41 | public void everyReaderHandleSeesChangesAfterRefresh() { 42 | final var map = LeftRightMap.create(); 43 | final var readers = List.of( 44 | map.readerFactory().createReader(), 45 | map.readerFactory().createReader(), 46 | map.readerFactory().createReader(), 47 | map.readerFactory().createReader() 48 | ); 49 | 50 | for (final var reader : readers) { 51 | assertNull(reader.get("a")); 52 | } 53 | 54 | try (final var writer = map.writer()) { 55 | writer.put("a", "b"); 56 | } 57 | 58 | for (final var reader : readers) { 59 | assertEquals(reader.get("a"), "b"); 60 | } 61 | } 62 | 63 | @Test 64 | public void readersOnDifferentThreadsSeeResults() { 65 | final var executor = Executors.newFixedThreadPool(8); 66 | final var map = LeftRightMap.create(); 67 | 68 | 69 | try (final var writer = map.writer()) { 70 | writer.put("a", "b"); 71 | } 72 | 73 | final List> readResults = new ArrayList<>(); 74 | for (int i = 0; i < 8; i++) { 75 | readResults.add(executor.submit(() -> map.threadSafeReader().get("a"))); 76 | } 77 | 78 | assertEquals( 79 | List.of("b", "b", "b", "b", "b", "b", "b", "b"), 80 | readResults.stream() 81 | .map(res -> { 82 | try { 83 | return res.get(); 84 | } catch (Exception e) { 85 | throw new RuntimeException(e); 86 | } 87 | }) 88 | .collect(Collectors.toList()) 89 | ); 90 | 91 | executor.shutdownNow(); 92 | } 93 | 94 | @Test 95 | public void writerSeesChangesImmediately() { 96 | final var map = LeftRightMap.create(); 97 | 98 | try (final var writer = map.writer()) { 99 | writer.put("a", "b"); 100 | writer.put("b", "c"); 101 | if (writer.get("a") != null) { 102 | writer.put("e", "f"); 103 | } 104 | 105 | assertEquals(writer.get("a"), "b"); 106 | assertEquals(writer.get("b"), "c"); 107 | assertEquals(writer.get("e"), "f"); 108 | } 109 | } 110 | 111 | @Test 112 | public void differentOperationsAreAppliedInOrder() { 113 | final var map = LeftRightMap.create(); 114 | final var reader = map.threadSafeReader(); 115 | final var writer = map.writer(); 116 | writer.put("a", "b"); 117 | writer.clear(); 118 | writer.put("c", "d"); 119 | writer.remove("c"); 120 | writer.put("e", "f"); 121 | writer.refresh(); 122 | 123 | assertEquals(reader.size(), 1); 124 | assertEquals(reader.get("e"), "f"); 125 | } 126 | 127 | @Test 128 | public void noIntermediateResultsAreSeenByReaders() { 129 | for (int time = 0; time < 5; time++) { 130 | final var map = LeftRightMap.create(); 131 | final var writer = map.writer(); 132 | writer.put("a", "b"); 133 | writer.refresh(); 134 | writer.put("a", "c"); 135 | 136 | final var executor = Executors.newFixedThreadPool(8); 137 | final List> readResults = new ArrayList<>(); 138 | for (int i = 0; i < 1000000; i++) { 139 | final var reader = map.threadSafeReader(); 140 | readResults.add(executor.submit(() -> reader.get("a"))); 141 | } 142 | 143 | try { // Pause to give the tasks enough time to see the bad value. 144 | Thread.sleep(10); 145 | } 146 | catch (InterruptedException e) { 147 | throw new RuntimeException(e); 148 | } 149 | 150 | writer.put("a", "d"); 151 | writer.refresh(); 152 | 153 | assertEquals( 154 | Set.of("b", "d"), // spawning the futures should always take long enough to see the final state. 155 | readResults.stream() 156 | .map(res -> { 157 | try { 158 | return res.get(); 159 | } catch (Exception e) { 160 | throw new RuntimeException(e); 161 | } 162 | }) 163 | .collect(Collectors.toSet()) 164 | ); 165 | 166 | executor.shutdownNow(); 167 | } 168 | } 169 | 170 | 171 | } 172 | --------------------------------------------------------------------------------