这个 Java 线程锁定概念不好吗?
Is this Java thread locking concept bad?
我将尝试通过示例简要解释我提出的线程锁定概念。考虑以下示例程序。
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
在这种情况下不起作用。我必须在主线程上提交之前锁定并释放执行线程上的锁。
Java 具有更高级别的同步抽象。一般来说,你应该真正避免 wait() 和 notifyAll(),它们太低级和复杂以至于无法正确使用和阅读。
在这种情况下,您可以在两个线程之间使用共享阻塞队列(synchronous queue 看起来适合我):
ExecutorService executorService = Executors.newSingleThreadExecutor();
Data data = new Data();
SynchronousQueue queue = new SynchronousQueue();
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));
}
queue.put(data);
}
});
doStuff();
doStuff();
data = queue.take();
for (int i = 0; i < 256; i++) {
data.set(i, (byte) (data.get(i) + 1));
}
doStuff();
doStuff();
}
基本同步似乎已经很好地涵盖了这一点:doStuff
期望对数据具有唯一访问权限,大概这样它就可以安全地修改整个数据。同时,doPrint
期望在稳定的数据上运行。在这两种情况下,数据是控制访问的状态单元,适当的锁定技术是在被访问的数据实例上同步。
public void runningInThread1() {
Data someData = getData(); // Obtain the data which this thread is using
doStuff(someData); // Update the data
// Looping and such omitted.
}
public void runningInThread2() {
Data someData = getData();
doPrint(someData); // Display the data
}
public void doStuff(Data data) {
synchronized ( data ) {
// Do some stuff to the data
}
}
public void doPrint(Data data) {
synchronized ( data ) {
// Display the data
}
}
或者,如果 doStuff
和 doPrint
作为 Data
的实例方法实现,则可以通过将 synchronized
关键字添加到实例方法。
你想让这个Data东西异步构建,主线程想要能够进行到某个点,就需要获取完成的对象。这就是 Futures 的用途,它为您提供对可能尚未完成的计算的引用。
将异步部分重写为 Callable,使其 returns 成为数据。
Callable<Integer> task = () -> {
Data data = new Data();
for (int i = 0; i < 256; i++) {
System.out.println("Data " + i + ": " + data.get(i));
}
return data;
};
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Data> future = executor.submit(task);
doStuff();
// ... main thread goes about its business
// when you get to a point where you need data,
// you can block here until the computation is done
Data data = future.get();
通过这种方式,Future 可以确保数据对象在线程中可见。
我找到了解决该问题的另一种方法,它也回答了我对@JB Nizet 的回答的评论。 ExecutorService#submit(Runnable)
returns a Future<?>
可用于等待任务准备就绪。如果多个提交应该是可能的,那么可以只创建一个 Queue<Future<?>> queue
,始终将 ExecutorService#submit(Runnable)
返回的 Future<?>
提供给 queue
并且在主线程应该等待的地方#poll().get()
整个queue
.
编辑:我在这里也找到了相关答案:
我将尝试通过示例简要解释我提出的线程锁定概念。考虑以下示例程序。
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
在这种情况下不起作用。我必须在主线程上提交之前锁定并释放执行线程上的锁。
Java 具有更高级别的同步抽象。一般来说,你应该真正避免 wait() 和 notifyAll(),它们太低级和复杂以至于无法正确使用和阅读。
在这种情况下,您可以在两个线程之间使用共享阻塞队列(synchronous queue 看起来适合我):
ExecutorService executorService = Executors.newSingleThreadExecutor();
Data data = new Data();
SynchronousQueue queue = new SynchronousQueue();
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));
}
queue.put(data);
}
});
doStuff();
doStuff();
data = queue.take();
for (int i = 0; i < 256; i++) {
data.set(i, (byte) (data.get(i) + 1));
}
doStuff();
doStuff();
}
基本同步似乎已经很好地涵盖了这一点:doStuff
期望对数据具有唯一访问权限,大概这样它就可以安全地修改整个数据。同时,doPrint
期望在稳定的数据上运行。在这两种情况下,数据是控制访问的状态单元,适当的锁定技术是在被访问的数据实例上同步。
public void runningInThread1() {
Data someData = getData(); // Obtain the data which this thread is using
doStuff(someData); // Update the data
// Looping and such omitted.
}
public void runningInThread2() {
Data someData = getData();
doPrint(someData); // Display the data
}
public void doStuff(Data data) {
synchronized ( data ) {
// Do some stuff to the data
}
}
public void doPrint(Data data) {
synchronized ( data ) {
// Display the data
}
}
或者,如果 doStuff
和 doPrint
作为 Data
的实例方法实现,则可以通过将 synchronized
关键字添加到实例方法。
你想让这个Data东西异步构建,主线程想要能够进行到某个点,就需要获取完成的对象。这就是 Futures 的用途,它为您提供对可能尚未完成的计算的引用。
将异步部分重写为 Callable,使其 returns 成为数据。
Callable<Integer> task = () -> {
Data data = new Data();
for (int i = 0; i < 256; i++) {
System.out.println("Data " + i + ": " + data.get(i));
}
return data;
};
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Data> future = executor.submit(task);
doStuff();
// ... main thread goes about its business
// when you get to a point where you need data,
// you can block here until the computation is done
Data data = future.get();
通过这种方式,Future 可以确保数据对象在线程中可见。
我找到了解决该问题的另一种方法,它也回答了我对@JB Nizet 的回答的评论。 ExecutorService#submit(Runnable)
returns a Future<?>
可用于等待任务准备就绪。如果多个提交应该是可能的,那么可以只创建一个 Queue<Future<?>> queue
,始终将 ExecutorService#submit(Runnable)
返回的 Future<?>
提供给 queue
并且在主线程应该等待的地方#poll().get()
整个queue
.
编辑:我在这里也找到了相关答案: