资讯专栏INFORMATION COLUMN

多线程编程:wait, notify, join, yield都有啥用?

lovXin / 2877人阅读

摘要:通知任一一个进入等待状态的线程,通知所有让调用线程阻塞在这个方法上,直到的线程完全执行完毕,调用线程才会继续执行。通知调度器,主动让出对的占用。

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

今天来理一理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的占用。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76009.html

相关文章

  • Java并发编程——线程基础查漏补缺

    摘要:告诉当前执行的线程为线程池中其他具有相同优先级的线程提供机会。不能保证会立即使当前正在执行的线程处于可运行状态。当达到超时时间时,主线程和是同样可能的执行者候选。下一篇并发编程线程安全性深层原因 Thread 使用Java的同学对Thread应该不陌生了,线程的创建和启动等这里就不讲了,这篇主要讲几个容易被忽视的方法以及线程状态迁移。 wait/notify/notifyAll 首先我...

    luqiuwen 评论0 收藏0
  • @Java | Thread & synchronized - [ 线程 基本使用]

    摘要:线程线程是进程中的一个实体,作为系统调度和分派的基本单位。下的线程看作轻量级进程。因此,使用的目的是让相同优先级的线程之间能适当的轮转执行。需要注意的是,是线程自己从内部抛出的,并不是方法抛出的。 本文及后续相关文章梳理一下关于多线程和同步锁的知识,平时只是应用层面的了解,由于最近面试总是问一些原理性的知识,虽说比较反感这种理论派,但是为了生计也必须掌握一番。(PS:并不是说掌握原理不...

    zhunjiee 评论0 收藏0
  • 我的面试准备过程--线程(更新中)

    摘要:但是,实际中无法保证达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。在大多数情况下,将导致线程从运行状态转到可运行状态,但有可能没有效果。 多线程编程 线程状态图 总是无法上传,稍后上传 常用函数 状态转换 运行中->阻塞 sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠 join() 等待t线程终止 使用方式 Thread t =...

    zoomdong 评论0 收藏0
  • Java 线程核心技术梳理(附源码)

    摘要:本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,的使用,定时器,单例模式,以及线程状态与线程组。源码采用构建,多线程这部分源码位于模块中。通知可能等待该对象的对象锁的其他线程。 本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock的使用,定时器,单例模式,以及线程状态与线程组。 写在前面 花了一周时...

    Winer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<