英文:
About Thread's wait()/ notify
问题
我试图编写一个关于如何使用wait()和notify()的示例,但似乎无法通过wait()来通知。
public class Transfer {
private int[] data;
private volatile int ptr;
private final Object lock = new Object();
public Transfer(int[] data) {
this.data = data;
this.ptr = 0;
}
public void send() {
while (ptr < data.length) {
synchronized (lock) {
try {
System.out.println("-----等待");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ptr++;
}
}
}
public void receive() {
while (ptr < data.length) {
synchronized (lock) {
System.out.println("当前值为:" + data[ptr]);
System.out.println("-----通知");
lock.notifyAll();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
////在主函数中:
int[] data = new int[] { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
Transfer tf = new Transfer(data);
Thread t1 = new Thread(() -> {
tf.receive();
});
Thread t2 = new Thread(() -> {
tf.send();
});
t2.start();
t1.start();
但结果是:
-----等待
当前值为:111
-----通知
当前值为:111
-----通知
[无限循环]
这不是我期望的结果,应该是:
当前值为:111
当前值为:222...
英文:
I was trying to write an example on how to use wait() and notify(), but seems that the wait() can't be notified
public class Transfer {
private int[] data;
private volatile int ptr;
private final Object lock = new Object();
public Transfer(int[] data) {
this.data = data;
this.ptr = 0;
}
public void send() {
while (ptr < data.length) {
synchronized (lock) {
try {
System.out.println("-----wait");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ptr++;
}
}
}
public void receive() {
while (ptr < data.length) {
synchronized (lock) {
System.out.println("current is " + data[ptr]);
System.out.println("-----notify");
lock.notifyAll();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
////in main()
int[] data = new int[] { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
Transfer tf = new Transfer(data);
Thread t1 = new Thread(() -> {
tf.receive();
});
Thread t2 = new Thread(() -> {
tf.send();
});
t2.start();
t1.start();
but the result is :
-----wait
current is 111
-----notify
current is 111
-----notify
[endless repeat]
this is not what I expected, it should be :
current is 111
current is 222...
答案1
得分: 2
你的代码存在特定问题,即你的锁保持时间过长。
首先,我将解释一下wait/notify的工作原理,这与监视器(synchronized
)的概念紧密相关,然后说明如何正确使用,以及为什么你可能根本不想使用它,因为它属于过低级别的操作。
'synchronized' 如何工作
当你使用 synchronized(x)
时,你会获得监视器,这个操作会执行以下三种情况之一。在所有情况下,x是一个引用,因此会根据引用进行跟踪,它指的是你通过跟踪找到的对象。
- 如果引用是 null,会立即抛出 NPE(NullPointerException)。
- 如果对象 x 指向的对象没有当前监视器,那么当前线程将成为监视器,监视器计数变为 1,代码继续执行。
- 如果对象 x 指向的对象有一个监视器,但是是当前线程的监视器,那么监视器计数递增,代码继续执行。
- 如果对象 x 指向的对象有一个监视器,但是是其他线程的监视器,那么线程会阻塞,直到监视器变为可用。一旦可用,不公平的骰子会出现,被掷出,决定所有争夺监视器的线程中哪个线程会获得它。所谓不公平是指没有任何保证,并且 JVM 可以自由地使用任何算法来决定谁“获胜”。如果你的代码依赖于公平性或某个确定的顺序,那么你的代码是有问题的。
- 在达到同步块的
}
时,监视器计数递减。如果计数达到 0,监视器将被释放(如果其他线程在等待,则按照第4点的情况开始争夺)。换句话说,在 Java 中,锁是可重入的。线程可以执行synchronized(a){synchronized(a){}}
,而不会与自己发生死锁。 - 是的,这根据 Java 内存模型建立了“先行发生”关系:由 synchronized 块仲裁的任何争斗也将确保由赢得争斗的一方明确先于其它内容编写的任何写操作,对于任何明确后续的内容都是可见的。
- 被标记为
synchronized
的方法在实例方法中等效于将代码包装在synchronized(this)
中,在静态方法中等效于将代码包装在synchronized(MyClass.class)
中。 - 在 Java 代码中,监视器不会被释放,也不能被更改,除非通过
}
机制;(在 j.l.Object 或其他地方没有public Thread getMonitor() {..}
之类的方法)- 特别是如果线程因其他原因(包括Thread.sleep
)而阻塞,监视器状态不会改变,线程会继续持有它,从而阻止所有其他线程获取它。唯一的例外是:
那么 wait/notify 如何与此相关呢?
- 要在 x 上等待/通知,你必须持有监视器。这样的代码
x.notify();
如果没有在synchronized(x)
块中包装,是不起作用的。 - 当你调用 wait() 时,监视器被释放,监视器计数被记住。调用
wait()
需要发生两件事情,然后它才能继续:等待需要被取消,可以通过超时、中断或通过 notify(All) 来取消,而且线程需要再次获取该监视器。如果是通过正常方式(通过 notify)完成的,根据定义,这将是一个争夺,因为调用 notify 的人肯定仍然持有该监视器。
这解释了为什么你的代码不起作用 - 你的“接收方”代码段在休眠期间保持了监视器。将休眠移出 synchronized 块。
通常如何使用这些
最佳的 wait/notifyAll 使用方式是不要对锁定和解锁的“流程”做太多假设。只有在获取监视器之后,才检查某个状态。如果状态需要等待某些事件发生,那么只有在这种情况下才开始 wait() 循环。将引发该事件的线程首先必须获取监视器,然后才能设置步骤以开始事件。如果不可能实现这一点,那没关系 - 设置一个故障转移,使使用 wait()
的代码使用超时(例如 wait(500L)
),以便如果出现问题,while 循环将解决问题。此外,实际上没有太多好的理由来使用 notify
,所以最好忘记它的存在。notify 对于它将解锁哪些内容没有任何保证,考虑到所有使用 wait 的线程都应该在任何情况下都检查他们等待的条件,无论 wait 的行为如何,总是应该调用 notifyAll。
所以,代码看起来是这样的... 假设我们正在等待某个文件的存在。
// 等待方:
Path target = Paths.get("/file-i-am-waiting-for.txt");
synchronized (lock) {
while (!Files.isRegularFile(target)) {
try {
lock.wait(1000L);
} catch (InterruptedException e) {
// 这个异常仅会在某个代码明确调用了 Thread.interrupt() 时发生。
// 因
<details>
<summary>英文:</summary>
The problem with your code specifically is that you are keeping your locks way too long.
I'll first explain how wait/notify works, which is intricately connected with the concept of the monitor (`synchronized`), then how to do it right, and then as an encore, that you probably don't want to use this at all, it's too low level.
## How does 'synchronized' work
When you write `synchronized(x)` you __acquire the monitor__ - this operation can do one of three things. In all cases, x is a reference, so the reference is followed, it's about the object you find by following it.
1. If the reference is null, this immediately throws NPE.
2. If the object x points at has no current monitor, this thread becomes the monitor, the monitor count becomes 1, and code continues.
3. If the object x points at has a monitor but it is this thread, then the monitor count is incremented and code continues.
4. If the object x points at has a monitor but it is another thread, the thread will block until the monitor becomes available. Once it is available, some unfair dice show up, are rolled, and determine which of all threads 'fighting' to acquire the monitor will acquire it. Unfair in the sense that there are no guarantees made and the JVM is free to use any algorithm it wants to decide who 'wins'. If your code depends on fairness or some set order, your code is broken.
5. Upon reaching the `}` of the synchronized block, the monitor count is decremented. If it hits 0, the monitor is released (and the fight as per #4 starts, if other threads are waiting). In other words, locks are 're-entrant' in java. A thread can write `synchronized(a){synchronized(a){}}` and won't deadlock with itself.
6. Yes, this establishes comes-before stuff as per the Java Memory Model: Any fights arbitrated by a synchronized block will also ensure any writes by things that clearly came before (as established by who wins the fight) are observable by anything that clearly came after.
7. A method marked as 'synchronized' is effectively equivalent to wrapping the code in `synchronized(this)` for instance methods, and `synchronized(MyClass.class)` for static methods.
8. Monitors are __not released__ and __cannot be changed__ in java code* except via that `}` mechanism; (there is no `public Thread getMonitor() {..}` in j.l.Object or anywhere else) - in particular if the thread blocks for any other reason, including `Thread.sleep`, the monitor status does not change - your thread continues to hold on to it and thus stops all other threads from acquiring it. With one exception:
So how does wait/notify factor into this?
1. to wait/notify on x you _MUST_ hold the monitor. this: `x.notify();`, unless it is wrapped in a `synchronized(x)` block, does not work.
2. When you wait(), __the monitor is released__, and the monitor count is remembered. a call to `wait()` __requires 2 things to happen__ before it can continue: The 'wait' needs to be cancelled, either via a timeout, or an interrupt, or via a notify(All), __and__ the thread needs to acquire that monitor again. If done normally (via a notify), by definition this is a fight, as whomever called notify neccessarily is still holding that monitor.
This then explains why your code does not work - your 'receiver' snippet holds on to the monitor while it sleeps. Take the sleep outside of the synchronized.
## How do you use this, generally
The best way to use wait/notifyAll is not to make too many assumptions about the 'flow' of locking and unlocking. __Only after__ acquiring the monitor, check some status. If the status is such that you need to wait for something to happen, then and only then start the wait() cycle. The thread that will cause that event to happen will first have to acquire the monitor and only then set steps to start the event. If this is not possible, that's okay - put in a failsafe, make the code that `wait()`s use a timeout (`wait(500L)` for example), so that if things fail, the while loop will fix the problem. Furthermore, there really is no good reason to ever use `notify` so forget that exists. notify makes no guarantees about what it'll unlock, and given that all threads that use wait ought to be checking the condition they were waiting for regardless of the behaviour of wait, notifyAll is always the right call to make.
So, it looks like this... let's say we're waiting for some file to exist.
// waiting side:
Path target = Paths.get("/file-i-am-waiting-for.txt");
synchronized (lock) {
while (!Files.isRegularFile(target)) {
try {
lock.wait(1000L);
} catch (InterruptedException e) {
// this exception occurs ONLY
// if some code explicitly called Thread.interrupt()
// on this thread. You therefore know what it means.
// usually, logging interruptedex is wrong!
// let's say here you intended it to mean: just exit
// and do nothing.
// to be clear: Interrupted does not mean:
// 'someone pressed CTRL+C' or 'the system is about to shutdown'.
return;
}
}
performOperation(target);
}
And on the 'file creation' side:
Path tgt = Paths.get("/file-i-am-waiting-for.txt");
Path create = tgt.getParent().resolve(tgt.getFileName() + ".create");
fillWithContent(create);
synchronized (lock) {
Files.move(create, tgt, StandardOpenOption.ATOMIC_MOVE);
lock.notifyAll();
}
The 'sending' (notifying) side is very simple, and note how we're using the file system to ensure that if the tgt file exists at all, it's fully formed and not a half-baked product. The receiving side uses a while loop: the notifying is itself NOT the signal to continue; it is merely the signal to re-check for the existence of this file. __This is almost always how to do this stuff__. Note also how all code involved with that file is always only doing things when they hold the lock, thus ensuring no clashes on that part.
## But.. this is fairly low level stuff
The `java.util.concurrent` package has superior tooling for this stuff; for example, you may want a latch here, or a ReadWriteLock. They tend to outperform you, too.
But even juc is low level. Generally threading works best if the comm channel used between threads is inherently designed around concurrency. DBs (with a proper transaction level, such as SERIALIZABLE), or message buses like rabbitmq are such things. Why do you think script kiddies fresh off of an 8 hour course on PHP can manage to smash a website together that actually does at least hold up, thread-wise, even if it's littered with security issues? Because PHP enforces a model where all comms run through a DB because PHP is incapable of anything else in its basic deployment. As silly as these handcuffs may sound, the principle is solid, and can be applied just as easily from java.
*) sun.misc.Unsafe can do it, but it's called Unsafe for a reason.
## Some closing best practices
* Locks should be private; this is a rule broken by most examples and a lot of java code. You've done it right: if you're going to use synchronized, it should probably be on `lock`, which is `private final Object lock = new Object();`. Make it `new Object[0]` if you need it to be serializable, which arrays are, and Objects aren't.
* if ever there is code in your system that does: `synchronized(a) { synchronized (b) { ... }}` and also code that odes: `synchronized(b) { synchronized (a) { ... }}` you're going to run into a deadlock at some point (each have acquired the first lock and are waiting for the second. They will be waiting forever. Be REAL careful when acquiring more than one monitor, and if you must, put in a ton of effort to ensure that you always acquire them in the same order to avoid deadlocks. Fortunately, jstack and such (tools to introspect running VMs) can tell you about deadlocks. The JVM itself, unfortunately, will just freeze in its tracks, dead as a doornail, if you deadlock it.
</details>
# 答案2
**得分**: 0
```java
class Transfer {
private int[] data;
private volatile int ptr;
private final Object lock = new Object();
public Transfer(int[] data) {
this.data = data;
this.ptr = 0;
}
public void send() {
while (ptr < data.length) {
synchronized (lock) {
try {
System.out.println("-----等待");
lock.notifyAll();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ptr++;
}
}
}
public void receive() {
while (ptr < data.length) {
synchronized (lock) {
System.out.println("当前为 " + data[ptr]);
System.out.println("-----通知");
try {
lock.notifyAll();
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
"Thread.sleep"不会释放锁。因此,你需要使用"lock.wait"来释放锁,让其他线程继续执行。然后在"send"方法中递增指针后,还应该发出通知,以便在接收方法中等待的线程能够继续执行。
英文:
class Transfer {
private int[] data;
private volatile int ptr;
private final Object lock = new Object();
public Transfer(int[] data) {
this.data = data;
this.ptr = 0;
}
public void send() {
while (ptr < data.length) {
synchronized (lock) {
try {
System.out.println("-----wait");
lock.notifyAll();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ptr++;
}
}
}
public void receive() {
while (ptr < data.length) {
synchronized (lock) {
System.out.println("current is " + data[ptr]);
System.out.println("-----notify");
try {
lock.notifyAll();
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
"Thread.sleep" does not release the lock. So you need "lock.wait" to release the lock and let other thread proceed. Then after "send" increment the pointer, it should also notify so that other thread who is stuck at receive can now proceed.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论