Java 多线程执行被阻止
Java Multithreading Execution Blocked
出于学习目的,我尝试实现队列数据结构 + Consumer/producer 线程安全的链,出于学习目的,我也没有使用 notify/wait 机制:
同步队列:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
return set;
}
synchronized public void enqueue(int val) {
this.val = val;
set = true;
}
synchronized public int dequeue() {
set = false;
return val;
}
}
消费者:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class Consumer implements Runnable {
SyncQueue queue;
public Consumer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
while(true) {
if(queue.isSet()) {
System.out.println(queue.dequeue());
}
}
}
}
制作人:
package syncpc;
import java.util.Random;
/**
* Created by Administrator on 01/07/2009.
*/
public class Producer implements Runnable {
SyncQueue queue;
public Producer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
Random r = new Random();
while(true) {
if(!queue.isSet()) {
queue.enqueue(r.nextInt() % 100);
}
}
}
}
主线:
import syncpcwn.*;
/**
* Created by Administrator on 27/07/2015.
*/
public class Program {
public static void main(String[] args) {
SyncQueue queue = new SyncQueue();
new Producer(queue, "PROCUDER");
new Consumer(queue, "CONSUMER");
}
}
这里的问题是,如果 isSet 方法不同步,我会得到这样的输出:
97,
55
并且程序只是继续 运行 而没有输出任何值。而如果 isSet 方法同步则程序正常工作。
我不明白为什么,没有死锁,isSet方法只是查询set实例变量而没有设置它,所以没有竞争条件。
set
需要 volatile
:
private boolean volatile set = false;
这确保所有读取器在写入完成时都能看到更新的值。否则他们最终会看到缓存的值。 this 关于并发的文章对此进行了更详细的讨论,还提供了使用 volatile
.
的不同模式的示例
现在您的代码可以使用 synchronized
的原因可能最好用一个例子来解释。 synchronized
方法可以写成如下(即它们等同于以下表示):
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
synchronized(this) {
return set;
}
}
public void enqueue(int val) {
synchronized(this) {
this.val = val;
set = true;
}
}
public int dequeue() {
synchronized(this) {
set = false;
return val;
}
}
}
这里实例是本身用作锁。这意味着只有线程可以持有该锁。这意味着任何线程都会总是获得更新的值,因为只有一个线程可以写入值,而一个线程想要读取set
将无法执行 isSet
直到 另一个线程 释放 对 this
的锁定, 此时 set
的值将被更新。
如果您想正确理解 Java 中的并发性,您真的应该阅读 Java: Concurrency In Practice(我认为某处也有免费的 PDF)。我仍在阅读这本书,因为还有很多我不理解或错误的地方。
正如 matt forsythe 评论的那样,当您有多个消费者时,您将 运行 陷入困境。这是因为它们都可以检查 isSet()
并发现有一个值要出列,这意味着它们都将尝试使同一个值出列。归结为这样一个事实,即您 真正 想要的是 "check and dequeue if set" 操作是有效的原子操作,但它不是您编写代码的方式。这是因为最初调用 isSet
的同一线程可能不一定是随后调用 dequeue
的同一线程。所以操作 作为一个整体 不是原子的,这意味着您必须同步整个操作。
您遇到的问题是可见性(或者更确切地说,缺乏可见性)。
如果没有任何相反的指示,JVM 将假定分配给一个线程中的变量的值不需要对其他线程可见。它可能会在以后有时可见(当这样做方便时),或者可能永远不会。 Java 内存模型定义了关于什么应该可见以及何时可见的规则,它们总结为 here。 (一开始可能有点枯燥和可怕,但理解它们绝对是至关重要的。)
因此,即使生产者将 set
设置为 true
,消费者仍会将其视为 false。您如何发布新值?
- 将字段标记为
volatile
。这适用于像 boolean
这样的原始值,对于引用你必须更加小心。
synchronized
不仅提供互斥,而且保证其中设置的任何值对于进入使用同一对象的 synchronized
块的任何人都是可见的。 (这就是为什么如果你声明 isSet()
方法 synchronized
一切正常的原因。)
- 使用线程安全库 class,例如
Atomic*
class 的 java.util.concurrent
在你的情况下 volatile
可能是最好的解决方案,因为你只更新一个 boolean
,所以默认情况下保证更新的原子性。
As @matt forsythe pointed out, there is also a TOCTTOU 您的代码也有问题,因为您的线程可能会在 isSet()
和 enqueue()/dequeue()
之间被另一个中断。
我假设当我们陷入线程问题时,第一步是确保两个线程都正常 运行。 (我知道他们会的,因为没有锁会造成死锁)
为此,您也可以在 enqueue 函数中添加一个 printf 语句。这将确保入队和出队线程 运行 正常。
那么第二步应该是 "set" 是共享资源,因此值切换是否足够好,以便代码可以 运行时尚
我想如果你能推理并把日志记录得足够好,你就能意识到问题所在。
出于学习目的,我尝试实现队列数据结构 + Consumer/producer 线程安全的链,出于学习目的,我也没有使用 notify/wait 机制:
同步队列:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
return set;
}
synchronized public void enqueue(int val) {
this.val = val;
set = true;
}
synchronized public int dequeue() {
set = false;
return val;
}
}
消费者:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class Consumer implements Runnable {
SyncQueue queue;
public Consumer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
while(true) {
if(queue.isSet()) {
System.out.println(queue.dequeue());
}
}
}
}
制作人:
package syncpc;
import java.util.Random;
/**
* Created by Administrator on 01/07/2009.
*/
public class Producer implements Runnable {
SyncQueue queue;
public Producer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
Random r = new Random();
while(true) {
if(!queue.isSet()) {
queue.enqueue(r.nextInt() % 100);
}
}
}
}
主线:
import syncpcwn.*;
/**
* Created by Administrator on 27/07/2015.
*/
public class Program {
public static void main(String[] args) {
SyncQueue queue = new SyncQueue();
new Producer(queue, "PROCUDER");
new Consumer(queue, "CONSUMER");
}
}
这里的问题是,如果 isSet 方法不同步,我会得到这样的输出:
97,
55
并且程序只是继续 运行 而没有输出任何值。而如果 isSet 方法同步则程序正常工作。
我不明白为什么,没有死锁,isSet方法只是查询set实例变量而没有设置它,所以没有竞争条件。
set
需要 volatile
:
private boolean volatile set = false;
这确保所有读取器在写入完成时都能看到更新的值。否则他们最终会看到缓存的值。 this 关于并发的文章对此进行了更详细的讨论,还提供了使用 volatile
.
现在您的代码可以使用 synchronized
的原因可能最好用一个例子来解释。 synchronized
方法可以写成如下(即它们等同于以下表示):
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
synchronized(this) {
return set;
}
}
public void enqueue(int val) {
synchronized(this) {
this.val = val;
set = true;
}
}
public int dequeue() {
synchronized(this) {
set = false;
return val;
}
}
}
这里实例是本身用作锁。这意味着只有线程可以持有该锁。这意味着任何线程都会总是获得更新的值,因为只有一个线程可以写入值,而一个线程想要读取set
将无法执行 isSet
直到 另一个线程 释放 对 this
的锁定, 此时 set
的值将被更新。
如果您想正确理解 Java 中的并发性,您真的应该阅读 Java: Concurrency In Practice(我认为某处也有免费的 PDF)。我仍在阅读这本书,因为还有很多我不理解或错误的地方。
正如 matt forsythe 评论的那样,当您有多个消费者时,您将 运行 陷入困境。这是因为它们都可以检查 isSet()
并发现有一个值要出列,这意味着它们都将尝试使同一个值出列。归结为这样一个事实,即您 真正 想要的是 "check and dequeue if set" 操作是有效的原子操作,但它不是您编写代码的方式。这是因为最初调用 isSet
的同一线程可能不一定是随后调用 dequeue
的同一线程。所以操作 作为一个整体 不是原子的,这意味着您必须同步整个操作。
您遇到的问题是可见性(或者更确切地说,缺乏可见性)。
如果没有任何相反的指示,JVM 将假定分配给一个线程中的变量的值不需要对其他线程可见。它可能会在以后有时可见(当这样做方便时),或者可能永远不会。 Java 内存模型定义了关于什么应该可见以及何时可见的规则,它们总结为 here。 (一开始可能有点枯燥和可怕,但理解它们绝对是至关重要的。)
因此,即使生产者将 set
设置为 true
,消费者仍会将其视为 false。您如何发布新值?
- 将字段标记为
volatile
。这适用于像boolean
这样的原始值,对于引用你必须更加小心。 synchronized
不仅提供互斥,而且保证其中设置的任何值对于进入使用同一对象的synchronized
块的任何人都是可见的。 (这就是为什么如果你声明isSet()
方法synchronized
一切正常的原因。)- 使用线程安全库 class,例如
Atomic*
class 的java.util.concurrent
在你的情况下 volatile
可能是最好的解决方案,因为你只更新一个 boolean
,所以默认情况下保证更新的原子性。
As @matt forsythe pointed out, there is also a TOCTTOU 您的代码也有问题,因为您的线程可能会在 isSet()
和 enqueue()/dequeue()
之间被另一个中断。
我假设当我们陷入线程问题时,第一步是确保两个线程都正常 运行。 (我知道他们会的,因为没有锁会造成死锁)
为此,您也可以在 enqueue 函数中添加一个 printf 语句。这将确保入队和出队线程 运行 正常。
那么第二步应该是 "set" 是共享资源,因此值切换是否足够好,以便代码可以 运行时尚
我想如果你能推理并把日志记录得足够好,你就能意识到问题所在。