Я попытаюсь кратко объяснить концепцию блокировки потоков, которую я придумал на примере.Рассмотрим следующий пример программы.
public class Main {
public static void main(String[] args) {
Data data = new Data();
while (true) {
doStuff();
doStuff();
for (int i = 0; i < 256; i++) {
System.out.println("Data " + i + ": " + data.get(i));
}
doStuff();
doStuff();
for (int i = 0; i < 256; i++) {
data.set(i, (byte) (data.get(i) + 1));
}
doStuff();
doStuff();
}
}
public static void doStuff() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Data {
private final byte[] data = new byte[256];
public byte get(int i) {
return data[i];
}
public void set(int i, byte data) {
this.data[i] = data;
}
}
Важно, чтобы только основной поток изменял data
.Теперь я хочу сделать цикл, который печатает data
асинхронно.
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Data data = new Data();
while (true) {
doStuff();
doStuff();
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 256; i++) {
System.out.println("Data " + i + ": " + data.get(i));
}
}
});
doStuff();
doStuff();
for (int i = 0; i < 256; i++) {
data.set(i, (byte) (data.get(i) + 1));
}
doStuff();
doStuff();
}
}
После отправки задачи в executorService
основной поток теперь может работать так, как нужно.Проблема в том, что основной поток может потенциально достичь точки, в которой он изменяет data
до того, как он был напечатан, но состояние data
должно быть напечатано при его отправке.
Я знаю, что в этом случае яЯ могу создать копию data
перед отправкой, которая распечатана, но на самом деле это не то, что я хочу сделать.Помните, что это всего лишь пример, и копирование может быть дорогой операцией в реальном коде.
Это решение, которое я придумал для этой проблемы.
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Data data = new Data();
Lock lock = new Lock(); // <---------------
while (true) {
doStuff();
doStuff();
lock.lock(); // <---------------
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 256; i++) {
System.out.println("Data " + i + ": " + data.get(i));
}
lock.unlock(); // <---------------
}
});
doStuff();
doStuff();
lock.waitUntilUnlock(); // <---------------
for (int i = 0; i < 256; i++) {
data.set(i, (byte) (data.get(i) + 1));
}
doStuff();
doStuff();
}
}
public class Lock {
private final AtomicInteger lockCount = new AtomicInteger();
public void lock() {
lockCount.incrementAndGet();
}
public synchronized void unlock() {
lockCount.decrementAndGet();
notifyAll();
}
public synchronized void waitUntilUnlock() {
while (lockCount.get() > 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
}
ТеперьОсновной поток может перейти к работе над другими вещами после отправки data
.По крайней мере, может, пока не достигнет точки, где он модифицирует data
.
Вопрос: это хороший или плохой дизайн?Или есть лучшая (уже существующая) реализация для этой проблемы?
Обратите внимание, что ReentrantLock
не будет работать в этом случае.Я должен заблокировать перед отправкой в основной поток и снять блокировку в потоке исполнителя.