多线程编程wait-notify-join-yield都有啥用

36次阅读

共计 5499 个字符,预计需要花费 14 分钟才能阅读完成。

多线程在开发知识中是一个很重要的部分,然而实际生产中却很少遇到真正需要自己去处理多线程编程里的那些复杂细节和问题,因为很多时候,都有一套“架构”或者一些“框架”帮大部分业务程序员隐藏了多线程的细节,大多时候只需要简单的实现各种业务逻辑即可。

今天来理一理 wait, notify, join, yield 这四个方法的作用。

这 4 个方法,其中 wait, notify 都是 Object 的方法,join 是 Thread 的实例方法,yield 是 Thread 的静态方法。

wait, notify 在之前的文章:xxxx 中我已经提到过,wait 将线程转换为 Waiting 状态,notify 唤醒一个在 Waiting 状态的线程。

咱们一个个来说。

Object.wait

文档上是这样描述的:

Causes the current thread to wait until either another thread invokes the Object#notify() method or the Object#notifyAll() method for this object, or a specified amount of time has elapsed.

它是说:导致当前线程进入 waiting,直到另一个线程调用 notify 或者 notifyAll 方法来唤醒它,或者是指定了等待时间。

也就是用 wait 的有参的重载方法wait(long),可以让线程至多等待一定的时间,这个时间过了之后,线程就自行恢复 runnable 状态了。

正确的使用方法是在 synchronized 里面使用,并且使用一个循环将它包起来。

synchronized (lock) {while (!condition) {lock.wait() // 进入 waiting 状态, 这行代码之后的代码将不会被执行
    }
}

为什么要使用一个循环呢?因为通常情况下,按照逻辑的要求是达到某种条件之前,我这个线程就不工作了,当条件满足后,别的线程来通知我,当别的线程通知我之后呢,我还要再 check 一下这个条件是否满足,如果不满足,还要继续进入 waiting 状态,这样逻辑上才是比较完备的。

Object.notify

Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object’s monitor by calling one of the {@code wait} methods.

唤醒一个 waiting 态的线程,这个线程呢,必须是用同一把锁进入 waiting 态的。

所以,notify 方法的通常使用方法为:

synchronized (lock) {lock.notify()
}

有人可能要问了:wait 和 notify 不在 synchronized 里面使用会怎么样?
我也好奇了这一点,然后实验了一把,发现会抛异常,运行时直接报错

java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)

Thread.join

join 方法是一个实例方法,先看看文档的定义:

//Waits for this thread to die.
public final void join() throws InterruptedException

它的意思是,调用 threadA.join()的线程,要进入 waiting 状态,一直到线程 threadA 执行完毕。
比如

public static void main() {Thread t1 = new Thread(…);
       t1.join();
       // 这行代码必须要等 t1 全部执行完毕,才会执行
}

Thread.yield

A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore
this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.
Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.

public static native void yield();

这个方法的意思是,告诉调度器,当前线程愿意放弃 cpu 的使用,愿意将 cpu 让给其它的线程。

用人话说就是:哎呀,我现在已经运行了那么久了,把机会留给别人吧,cpu 你快去运行一下其他线程吧,我歇一会。

但是按文档上的描述,这只是对调度器的一个暗示。也就是说,具体会发生什么,还要看调度器是如何处理的。

所以我又来捏造需求了。我们先看看下面的代码会发生什么:

public static void main(String[] arg) {Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {for (int i = 0; i < 10000; i++) {count++;}
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {for (int i = 0; i < 10000; i++) {count++;}
        }
    });
    
    t1.start();
    t2.start();}

两个线程一起给 count 自增 10000 次,由于没有加锁,和自增也不是一个原子操作,这样就会导致,两个线程都自增 10000 次,最后 count 的结果,一定是小于 20000 的一个数。

等等!等一下!自增不是原子操作是怎么回事,这代码不是只有一行吗?

大家都知道代码最终会被翻译为指令,由 cpu 去执行,一条指令是原子的,但是一行代码被翻译成多条指令,那么也就会被多个线程交替进行,这也就是多线程编程常见的问题。

自增的代码可以用过 idea 的工具查看到。

GETSTATIC thread/TestThreadFunction.count : I
ICONST_1
IADD
PUTSTATIC thread/TestThreadFunction.count : I

可以看到,它被拆分成了四条执行去执行。

这个代码的执行结果就是,最后的结果是小于 20000 的。

那么,我们现在设计一下,我希望通过上面提到的方法,让两个线程交替的执行,这样不就可以稳定的自增到 20000 了吗?

具体怎么做呢,看下面的代码:

public static int count = 0;

public static final Object object = new Object();

public static void main(String[] arg) {Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {for (int i = 0; i < 10000; i++) {synchronized (object) {object.notify();
                        object.wait();}
                    count++;
                    System.out.println("t1" + count);
                }
                synchronized (object) {object.notify();
                }
            } catch (Throwable e) {}}
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {for (int i = 0; i < 10000; i++) {synchronized (object) {object.notify();
                        object.wait();}
                    count++;
                    System.out.println("t2" + count);
                }
                synchronized (object) {object.notify();
                }
            } catch (Throwable e) {}}
    });

    t1.start();
    t2.start();

    System.out.println("count:" + count);

}

首先第一个线程(t1)进入了同步锁 object,调用 notify 方法,通知别的线程起来干活,但此时没有任何作用,接下来调用 wait,让自己进入 waiting 状态。

接着第二个线程(t2)自然而然就要干起活来,它先调用了 notify 方法,触发了一次唤醒,然后调用 wait 方法也进入了 waiting 状态。

t1 收到了 notify 的唤醒,退出临界区,开始给 count 自增,本次循环结束,重新 notify,wait 后进入 waiting 状态。

t2 被这个 notify 所唤醒,开始给 count 自增,本次循环结束,接着重复一样的过程。

……

就这样,两个线程交替的执行了起来。

最终我们得到的结果是这样的:

count: 0
t1 1
t2 2
t1 3
t2 4
t1 5
t2 6
t1 7
t2 8
t1 9
t2 10
t1 11
... // 此处省略
t2 19998
t1 19999
t2 20000

我们发现一个问题,就是主线程的最后面的输出,先执行了,输出了 0,这是怎么回事呢?

这是由于两个工作线程还没开始工作,主线程就执行完毕了。那么,我们希望在两个线程执行完毕后,主线程再输出一下结果,这个事情可以做到吗?

我希望一个线程工作完毕了,我再继续执行

这个不就是 join 的作用吗?

于是我们的代码可以在 start 两个线程后,加上 join,再输出。

...// 这部分相同,省略
t1.start();
t2.start();

try {t1.join();
    t2.join();} catch (InterruptedException e) {e.printStackTrace();
}

System.out.println("count:" + count);

这样的执行结果就是主线程的输出在最后了。

... // 省略
t1 19997
t2 19998
t1 19999
t2 20000
count: 20000

接下来我们探讨一下 Thread.yield 的实际作用

先将代码改写为下面简单的,通过 synchronized 关键字进行同步的写法

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {for (int i = 0; i < 1000; i++) {synchronized (object) {
                count++;
                System.out.println("t111111" + count);
            }
        }
    }
});

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {for (int i = 0; i < 1000; i++) {synchronized (object) {
                count++;
                System.out.println("t2" + count);
            }
        }
    }
});

我们可以通过代码的输出,观察到,线程的调度是非常的紧密的,就是说,总是一段时间 t1 一直在执行,然后 t2 再紧密的执行一段时间。

... // 省略
t111111 153
t111111 154
t111111 155
t111111 156
t111111 157
t111111 158
t111111 159
t111111 160
t111111 161
t111111 162
t111111 163
t111111 164
t111111 165
t111111 166
t2 167
t2 168
t2 169
t2 170
t2 171
t2 172
t2 173
t2 174
t2 175
t2 176
t2 177
t2 178
t2 179
t2 180
t2 181
t2 182
... // 省略

t1 连续执行了 166 次,才轮到 t2 来执行。一旦 t2 开始执行,就会一直抢占 cpu 一段时间。

我们现在加上 Thread.yield 方法试试

for (int i = 0; i < 1000; i++) {synchronized (object) {
        count++;
        System.out.println("t2" + count);
    }
    Thread.yield(); // 加在这里}

大致的可以看到,线程对 cpu 的抢占,变得更加谦让了

t111111 1
t2 2
t2 3
t2 4
t111111 5
t2 6
t2 7
t2 8
t111111 9
t111111 10
t2 11
t2 12
t111111 13
t111111 14
t111111 15
t2 16
t2 17
t2 18
t111111 19
t111111 20
t2 21
t111111 22
t2 23
t2 24
t111111 25
t2 26
t111111 27
t2 28
t111111 29
t111111 30
t2 31
t2 32
... // 省略

总结

Object.wait 让线程进入 wait 状态,必须要其他线程唤醒,或者是传入了时间长度的 wait 方法,将至多等待传入的时间长度后自动唤醒。

Object.notify 通知任一一个进入等待状态的线程,notifyAll 通知所有

Thread.join 让调用线程阻塞在这个方法上,直到 join 的线程完全执行完毕,调用线程才会继续执行。

Thread.yield 通知调度器,主动让出对 cpu 的占用。

如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君

正文完
 0