Skip to content

Instantly share code, notes, and snippets.

@bdkosher
Last active December 18, 2015 16:59
Show Gist options
  • Save bdkosher/5815704 to your computer and use it in GitHub Desktop.
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).
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