Last active
December 18, 2015 16:59
-
-
Save bdkosher/5815704 to your computer and use it in GitHub Desktop.
JUnit test to demonstrate the necessity of synchronizing both reads and writes to a shared resource (in this case, a java.io.File).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Random; | |
import java.util.concurrent.Callable; | |
import java.util.concurrent.ExecutionException; | |
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.Future; | |
import java.util.concurrent.TimeUnit; | |
import org.junit.Test; | |
import static org.junit.Assert.assertEquals; | |
/** | |
* Proves the necessity of synchronizing both reads and writes of Files. The only test that consistently | |
* succeeds is the one where both the read and write access are synchronized. | |
* | |
* @author Joe Wolf | |
*/ | |
public class SimultaneousFileAccessTest { | |
/** | |
* Specifies how much data to write to the temp files. | |
*/ | |
private static final int FILE_SIZE_IN_BYTES = 128; | |
/** | |
* An object to synchronize on when accessing files. | |
*/ | |
private final Object readWriteLock = new Object(); | |
/** | |
* Reads the file and returns the contents of the bytes written as a Hex string. | |
*/ | |
private String readFile(File file) throws IOException { | |
StringBuilder sb = new StringBuilder(); | |
try (InputStream in = new FileInputStream(file)) { | |
int byteRead; | |
while ((byteRead = in.read()) != -1) { | |
sb.append(Integer.toHexString(byteRead)); | |
} | |
} | |
return sb.toString(); | |
} | |
/** | |
* Writes random data to the given file and returns the bytes written as a Hex string. | |
*/ | |
private String writeRandomDataToFile(File file) throws IOException { | |
Random rand = new Random(); | |
StringBuilder sb = new StringBuilder(); | |
byte[] buffer = new byte[1]; | |
try (OutputStream out = new FileOutputStream(file)) { | |
for (int i = 0; i < FILE_SIZE_IN_BYTES; ++i) { | |
rand.nextBytes(buffer); | |
out.write(buffer); | |
sb.append(Integer.toHexString(buffer[0] & 0xFF)); // "unsign" the byte | |
} | |
} | |
return sb.toString(); | |
} | |
/** | |
* Creates a task for writing to the given file; returns the content written. | |
*/ | |
private Callable<String> writeTask(final File file) { | |
return new Callable<String>() { | |
@Override | |
public String call() throws Exception { | |
return writeRandomDataToFile(file); | |
} | |
}; | |
} | |
/** | |
* Creates a task for reading from the given file; returns the content read. | |
*/ | |
private Callable<String> readTask(final File file) { | |
return new Callable<String>() { | |
@Override | |
public String call() throws Exception { | |
return readFile(file); | |
} | |
}; | |
} | |
/** | |
* Creates a task for writing to the given file; returns the content written. | |
* Must obtain a lock on the readWriteLock in order to write. | |
*/ | |
private Callable<String> writeSyncTask(final File file) { | |
return new Callable<String>() { | |
@Override | |
public String call() throws Exception { | |
synchronized (readWriteLock) { | |
return writeRandomDataToFile(file); | |
} | |
} | |
}; | |
} | |
/** | |
* Creates a task for reading from the given file; returns the content read. | |
* Must obtain a lock on the readWriteLock in order to read. | |
*/ | |
private Callable<String> readSyncTask(final File file) { | |
return new Callable<String>() { | |
@Override | |
public String call() throws Exception { | |
synchronized (readWriteLock) { | |
return readFile(file); | |
} | |
} | |
}; | |
} | |
/** | |
* Helper method to generate a new temp file to write to/read from. | |
*/ | |
private File newTempFile() throws IOException { | |
File file = File.createTempFile("SimultaneousFileAccessTest", ".tmp"); | |
file.deleteOnExit(); | |
return file; | |
} | |
/** | |
* Executes the two given tasks using a thread pool of size two. Returns the results of the | |
* tasks in order of task completion. The tasks are attempted to be scheduled so that the | |
* write occurs first. | |
*/ | |
private String[] runReadWriteTasks(Callable<String> writeTask, Callable<String> readTask) | |
throws ExecutionException, InterruptedException { | |
ExecutorService service = Executors.newFixedThreadPool(2); | |
// XXX: does ExecutorService API guarantee tasks will always be invoked in a particular order? | |
List<Future<String>> results = service.invokeAll(Arrays.asList(writeTask, readTask)); | |
service.shutdown(); | |
return new String[]{ results.get(0).get(), results.get(1).get() }; | |
} | |
@Test | |
public void testSyncReadAndWrite() throws Exception { | |
File file = newTempFile(); | |
String[] results = runReadWriteTasks(writeSyncTask(file), readSyncTask(file)); | |
assertEquals("Synchronized read/write", results[0], results[1]); | |
} | |
@Test | |
public void testUnsyncReadAndWrite() throws Exception { | |
File file = newTempFile(); | |
String[] results = runReadWriteTasks(writeTask(file), readTask(file)); | |
assertEquals("Unsynchronized read/write", results[0], results[1]); // may fail! | |
} | |
@Test | |
public void testSyncWriteAndUnsyncRead() throws Exception { | |
File file = newTempFile(); | |
String[] results = runReadWriteTasks(writeSyncTask(file), readTask(file)); | |
assertEquals("Mismatched unsync read/sync write", results[0], results[1]); // may fail! | |
} | |
@Test | |
public void testUnsyncWriteAndSyncRead() throws Exception { | |
File file = newTempFile(); | |
String[] results = runReadWriteTasks(writeSyncTask(file), readTask(file)); | |
assertEquals("Mismatched sync read/unsync write", results[0], results[1]); // may fail! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment