notify和notifyAll的区别,以及sleep、wait和join的区别
1、notify()和notifyAll()的区别
同步方法和同步代码块:
在定义同步代码块时必须显式指定锁对象,锁对象可以是普通对象,也可以是类名.class对象,如synchronized(obj)或synchronized(Object.class),锁对象就是小括号中指定的对象。
在定义同步方法时,并没有显式指定锁对象。如果是实例方法,则锁对象就是调用该方法的对象,如果是类方法,即静态方法,则锁对象就是静态方法所在的类。
对于实例同步方法的锁,哪个对象调用同步方法,同步方法的锁就是这个对象。
先说两个概念:锁池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中,锁池中的线程会去竞争该对象的锁。
等待池:假设线程A拥有某个对象的锁,执行该对象的一个synchronized方法,在该方法中调用了wait()方法,当线程A执行到wait()方法这一行代码时,线程A就会释放该对象的锁,并进入到该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
再来说notify和notifyAll的区别
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,只有当线程执行对象的synchronized方法中调用对象的wait()方法,它才会进入到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会随机唤醒一个线程,随机将一个线程由等待池移到锁池。
在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁。只有等到执行notify()方法的线程退出synchronized代码块/方法后,当前线程才会释放锁,而相应的呈wait状态的线程才可以去争取该对象锁。
当线程终止时,会调用自身的notifyAll方法,唤醒所有等待该线程对象锁的线程去尝试获取锁。
2、sleep、wait和join的区别
2.1、sleep与wait方法的区别
sleep方法和wait方法都可以让线程暂停执行,两者的主要区别是调用sleep方法的线程暂停的时间到了之后会自动恢复到可运行状态,而调用wait方法(不指定时间)的线程需要等待其他线程调用notify或notifyAll方法之后才能变成可运行状态。join方法也有让线程暂停执行的作用,让当前线程暂停,让调用join方法的线程执行完了之后,当前线程再接着执行。join方法是Thread类中的方法。
1、sleep()方法是Thread类中的静态方法,而wait()方法是属于Object类中的。
2、调用sleep()方法的线程会暂停执行指定的时间,让出cpu给其他线程,但线程不会释放对象锁。当指定的时间到了,该线程会自动恢复到可运行状态,与其他线程一起竞争CPU执行权。
调用对象wait()方法的线程会放弃对象锁,进入此对象的等待池中,只有针对此线程调用同一个对象的notify()或notifyAll()方法后,该线程才进入对象锁池中,准备获取对象锁进入可运行状态,与其他线程一起竞争CPU执行权。(注意此处说的对象通常是指普通对象,如果是线程对象,可以不用显式调用线程对象的notify或notifyAll方法,线程运行结束时会自动调用自身的notifyAll方法。)
3、sleep方法可以在任何地方使用,而wait,notify和notifyAll只能在同步方法或者同步块里面使用,否则抛异常:java.lang.IllegalMonitorStateException。
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
对于java.lang.IllegalMonitorStateException异常,此处多说一点:
同步方法或同步代码块上锁定的对象(即线程持有的锁对象),必须与调用wait,notify和notifyAll方法的对象是同一个对象,否则就会抛出IllegalMonitorStateException异常。
是谁在调用wait方法?调用谁的wait方法?是谁在调用sleep方法?
package com.tx.study.others.thread;public class WaitThread2 {public static void main(String[] args) {Calculate cal = new Calculate();Runnable r1 = new Runnable() {@Overridepublic void run() {System.out.println(String.format("子线程[%s]开始运行,求和", Thread.currentThread().getName()));int sum = cal.sum(2, 3);System.out.println("sum=======" + sum);}};Runnable r2 = new Runnable() {@Overridepublic void run() {System.out.println(String.format("子线程[%s]开始运行,乘积", Thread.currentThread().getName()));int product = cal.product(2,3);System.out.println("product======" + product);}};Runnable r3 = new Runnable() {@Overridepublic void run() {System.out.println(String.format("子线程[%s]开始运行,求差", Thread.currentThread().getName()));int minus = cal.minus(3,2);System.out.println("minus==========" + minus);}};Thread t1 = new Thread(r1,"线程r1");Thread t2 = new Thread(r2,"线程r2");t1.start();cal.waitOneSecond();t2.start();Thread t3 = new Thread(r3,"线程r3");t3.start();System.out.println("主线程结束============");}}class Calculate{public int sum(int a, int b){synchronized (this){int c = a + b;System.out.println(String.format("当前线程[%s]获得锁对象[%s]求和后等待被唤醒。",Thread.currentThread().getName(),this.hashCode()));try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(String.format("当前线程[%s]被唤醒", Thread.currentThread().getName()));return c;}}public int product(int a, int b){synchronized (this) {System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),this.hashCode()));this.waitOneSecond();int c = a * b;this.notify();System.out.println(String.format("当前线程[%s]计算乘积后唤醒其他线程。", Thread.currentThread().getName()));return c;}}public synchronized int minus(int a, int b){System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),this.hashCode()));this.waitOneSecond();int c = a - b;System.out.println(String.format("当前线程[%s]计算求差。", Thread.currentThread().getName()));return c;}public void waitOneSecond(){try {System.out.println(String.format("当前线程[%s]等待1秒钟", Thread.currentThread().getName()));Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
输出结果(可能的一种输出结果,多线程程序的每次输出结果不一定相同):
子线程[线程r1]开始运行,求和
当前线程[main]等待1秒钟
当前线程[线程r1]获得锁对象[1199746180]求和后等待被唤醒。
主线程结束============
子线程[线程r3]开始运行,求差
子线程[线程r2]开始运行,乘积
当前线程[线程r3]获得锁对象[1199746180]。
当前线程[线程r3]等待1秒钟
当前线程[线程r3]计算求差。
minus==========1
当前线程[线程r2]获得锁对象[1199746180]。
当前线程[线程r2]等待1秒钟
当前线程[线程r2]计算乘积后唤醒其他线程。
product======6
当前线程[线程r1]被唤醒
sum=======5
是谁在调用wait方法?调用谁的wait方法?让谁处于等待状态?
答:是在某个同步方法或同步代码块中调用锁对象的wait方法,是锁对象在调用自己的wait方法。在同步方法中调用锁对象的wait方法,使得获取锁对象的线程释放锁,进入等待状态。由于wait、notify和notifyAll方法都是Object类中的final方法,所以这些方法不能被子类重写。另外,Object是所有类的超类,因此每个类中都有wait、notify和notifyAll方法。当在某个同步方法或同步块中调用了锁对象的wait方法,那么获取锁对象的线程就会进入锁对象的等待池中,释放获得的对象锁,其他线程就可以获得这个对象锁,直到有线程在其他同步方法或同步块中调用了同一个锁对象的notify或notifyAll方法,在锁对象等待池中的线程才能被转移到锁池中,准备获得锁对象进入可运行状态,与其他线程一起竞争CPU执行权。
wait(time)方法让线程等待指定的时间,直到时间到了,该线程自动醒来,或者在指定时间完成之前,其他线程调用notify或notifyAll方法唤醒该线程,然后该线程准备获得对象锁进入可运行状态。
wait方法的源码:
public final void wait() throws InterruptedException {wait(0); //调用本地的wait(long timeout)方法
}public final void wait(long timeout, int nanos) throws InterruptedException {if (timeout < 0) {throw new IllegalArgumentException("timeout value is negative");}if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException("nanosecond timeout value out of range");}if (nanos > 0) {timeout++;}wait(timeout); //调用本地的wait(long timeout)方法
}//本地的wait方法
//如果timeout为零,则不考虑实际时间,在获得通知前该线程将一直等待
public final native void wait(long timeout) throws InterruptedException;
同样的,不是线程在调用sleep方法,而是Thread类在调用sleep方法。由于sleep是Thread的静态方法,所以通常是通过Thread类进行调用(也可创建Thread对象进行调用)。若想让某个线程暂停指定的时间,只需要在该线程中的任何一个地方使用Thread.sleep(time)即可让该线程睡眠指定时间time。例如,下面就是让主线程暂停1000ms,如果主线程获得对象锁,并不释放获得的锁。
public class Run {public static void main(String[] args) {try {Object lock = new Object();ThreadA a = new ThreadA(lock);a.start();Thread.sleep(1000);ThreadB b = new ThreadB(lock);b.start();} catch (InterruptedException e) {e.printStackTrace(); }}
}
sleep方法的源码:
public static void sleep(long millis, int nanos) throws InterruptedException {if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException("nanosecond timeout value out of range");}if (nanos >= 500000 || (nanos != 0 && millis == 0)) {millis++;}sleep(millis); //调用native的sleep方法
}//本地的sleep方法
public static native void sleep(long millis) throws InterruptedException;
2.2、sleep与join方法的区别
sleep()和join()有什么区别?
join方法的作用是:让调用join方法的线程对象执行run方法,而让当前线程阻塞等待(不是让调用join方法的线程阻塞等待),等待调用join方法的线程执行完了run方法之后,当前线程再继续向下执行。
由于join的内部调用了wait()方法,所以调用join()方法会使当前线程释放对象锁(注意:释放的是被等待线程对象的锁,如下例中的线程对象a)。而调用sleep方法的线程不会释放锁。
例如下面代码,在主线程main中创建并启动了a线程,a线程调用join方法,主线程等待a线程执行(主线程会释放掉拥有的线程对象a的锁),a线程执行完了run方法之后,主线程才能继续向下执行。可以理解为将a线程合并到主线程中了。
public class JoinTest {public static void main(String[] args) {Object lock = new Object();ThreadA a = new ThreadA(lock);
//对象a是个线程,在调用线程的join方法之前,该线程必须启动,否则当前线程(如此处的main线程)不会被阻塞。另外,如果调用join方法的线程(如此处的对象a)在调用join方法之前,已经执行完了run方法,线程不再存活,当前线程就不会被阻塞。a.start();synchronized(a){//同步代码块,锁对象是a,主线程获取了这个锁System.out.println("主线程执行同步代码块");} //主线程执行完了同步代码块,释放a锁try {System.out.println("主线程等待a线程执行完了再执行");a.join();System.out.println("a线程执行完了,主线程继续向下执行");//省略其他代码} catch (InterruptedException e) {e.printStackTrace();}}}class ThreadA extends Thread{private Object lock;public ThreadA(Object lock) {super();this.lock = lock;}@Overridepublic void run(){System.out.println("ThreadA线程执行run方法");try {sleep(3000);System.out.println("ThreadA线程等待3秒");} catch (InterruptedException e) {e.printStackTrace();}}
}
输出结果:
主线程执行同步代码块
主线程等待a线程执行完了再执行
ThreadA线程执行run方法
ThreadA线程等待3秒
a线程执行完了,主线程继续向下执行
结合join方法的源码对上例进行分析,在主线程中a线程对象调用了join()方法,接着调用a.join(0)方法,即调用join(long millis)方法,由于该方法是被synchronized修饰的同步方法,锁对象是线程对象a(因为是对象a调用的该同步方法),目前只有主线程调用这个同步方法,所以主线程获取了这个同步方法的锁对象。接着通过isAlive方法(由于是a调用的join方法,所以调用isAlive方法的也是对象a)判断线程a是否存活,如果线程a存活,则调用wait(0)方法(也是线程对象a在调用wait方法)。因为是在主线程中调用的a.join()方法,所以此处调用a.wait(0)方法也是在主线程中,由上面分析的wait方法的作用可知,主线程将进入对象a的等待池中,并释放掉拥有的对象a的锁。对象的wait(0)方法的作用是让线程一直等待,直到其他线程针对该线程调用同一个对象的notify或notifyAll方法。如果调用wait()方法的对象是一个线程对象,就会执行该线程对象的run方法,则可以无需显示调用线程对象的notify或notifyAll方法,因为在线程运行结束时,在run方法中会自动调用自身的notifyAll方法(这是在底层native方法中实现的),唤醒所有等待该线程对象锁的线程去尝试获取锁。这就是join方法的原理。上例中在主线程main中调用了线程对象a的join()方法,join()方法中调用了wait()方法,理论上需要显式调用对象a的notify或notifyAll方法才能唤醒主线程,但是等到线程a执行结束之后,主线程会自动苏醒,从a.join()代码之后继续执行剩余的代码。
对于join(long millis)方法,如果参数millis不为0,则线程等待了millis时间后,线程会自动醒来,即使调用join方法的线程仍然活着,等待线程将不再等待,尝试获取CPU执行权,继续向下执行。
join方法的源码:(join是Thread类中的一个方法)
public final void join() throws InterruptedException {join(0);
}//哪个对象调用同步方法,同步方法的锁就是这个对象
public final synchronized void join(long millis)throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) { //当调用join方法的线程执行结束,则跳出循环wait(0); //如果time为0,则线程将一直等待}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay); //线程等待delay时间后自动醒来now = System.currentTimeMillis() - base;}}
}
由于join的内部在同步方法中调用了wait()方法,在进入同步方法时当前线程需要获取锁对象,锁对象就是调用join方法的线程对象,也就是当前线程会获取调用join方法的线程对象作为锁对象进入到同步方法中。而在同步方法中调用wait方法时,当前线程又会释放掉刚刚获取到的锁对象,所以调用join()方法会使当前线程释放对象锁(注意:释放的是被等待线程对象的锁,如上例中的线程对象a)。如果当前线程还持有其他的锁对象,并不会释放。
示例1:
package com.tx.study.others.thread;public class JoinThread {public static void main(String[] args) {Object obj = new Object();Thread t1 = new Thread("thread-t1") {@Overridepublic void run() {synchronized (obj){System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),obj.hashCode()));waitTime(100);System.out.println(String.format("当前线程[%s]执行结束。",Thread.currentThread().getName()));}}};Runnable r2 = new Runnable() {@Overridepublic void run() {try {synchronized (obj){System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),obj.hashCode()));waitTime(100);t1.join();System.out.println(String.format("当前线程[%s]执行结束。",Thread.currentThread().getName()));}} catch (InterruptedException e) {e.printStackTrace();}}};Thread t2 = new Thread(r2,"thread-r2");t2.start();waitTime(100);t1.start();}private static void waitTime(long time){try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}}
输出结果:
上例中创建了两个子线程t1和t2,线程名称分别是thread-t1和thread-r2,主线程启动之后调用了线程t2的start方法,启动t2线程,随后主线程等待100ms,交出CPU执行权。线程t2运行run方法,获取到同步代码块上的锁对象,即obj对象,进入到同步代码块中,输出第一条信息到控制台,随后也等待100ms,交出CPU执行权。接着主线程获得CPU执行权,执行t1.start()启动t1线程,主线程执行完毕。假设此时线程t1获得CPU执行权(即使t2获得CPU执行权,效果也是一样的,因为在t2线程中调用了t1.join方法,同样是让线程t1运行),线程t1执行自己的run方法,在进入同步代码块时发现锁对象(即此处的obj对象)被线程t2持有,线程t1就会交出CPU执行权,由可运行状态(RUNNABLE)转为阻塞状态(BLOCKED),进入锁对象的锁池中,等待下一次获取锁对象。接着线程t2获得CPU执行权,从等待语句之后继续执行t1.join()代码。由上面分析可知,在join方法源码的同步方法中调用了wait方法,线程t2在进入该同步方法时会获取到线程t1对象作为锁对象,此时线程t2拥有两把锁:obj对象和t1对象。紧接着运行到join方法中的wait方法处,线程t2释放掉了t1对象这把锁,进入到t1对象的等待池中,处于WAITING等待状态,让线程t1执行自己的run方法(由于t1对象是线程对象,无需显式调用t1对象的notify或notifyAll方法来唤醒线程t2,等到线程t1运行结束,线程t2会自动苏醒)。注意:线程t2只是释放掉了t1对象这把锁,并未释放obj对象这把锁。由于线程t1在执行自己的run方法进入同步代码块时,一直获取不到锁对象obj,就一直被阻塞,处于BLOCKED状态。而线程t2又要等着线程t1运行完run方法之后才能苏醒,所以程序就会卡在这里无法运行。
下面通过jstack命令查看程序的堆栈信息,来验证上面的分析是否正确。
通过JoinThread程序的堆栈信息可知,上面的分析是正确的。
示例2:
下面将JoinThread程序的代码稍加修改看一下效果。将线程t1的run方法中的锁对象换成this,将线程t2的run方法中的锁对象换成t1,再次运行程序看一下输出结果。
public class JoinThread {public static void main(String[] args) {Object obj = new Object();Thread t1 = new Thread("thread-t1") {@Overridepublic void run() {synchronized (this){System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),this.hashCode()));waitTime(100);System.out.println(String.format("当前线程[%s]执行结束。",Thread.currentThread().getName()));}}};Runnable r2 = new Runnable() {@Overridepublic void run() {try {synchronized (t1){System.out.println(String.format("当前线程[%s]获得锁对象[%s]。",Thread.currentThread().getName(),t1.hashCode()));waitTime(100);t1.join();System.out.println(String.format("当前线程[%s]执行结束。",Thread.currentThread().getName()));}} catch (InterruptedException e) {e.printStackTrace();}}};Thread t2 = new Thread(r2,"thread-r2");t2.start();
//防止t2都运行完了,t1可能还未启动,此时根据join源码中的isAlive方法判断t1是否存活,如果存活,则t2等待,否则t2不等待。由于t1还未启动,所以t2不会等待。//waitTime(100); t1.start();}private static void waitTime(long time){try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}}
输出结果:
当前线程[thread-r2]获得锁对象[449120244]。
当前线程[thread-t1]获得锁对象[449120244]。
当前线程[thread-t1]执行结束。
当前线程[thread-r2]执行结束。
当t2都运行完了,t1还未启动时就会输出如下结果:
当前线程[thread-r2]获得锁对象[1533661924]。
当前线程[thread-r2]执行结束。
当前线程[thread-t1]获得锁对象[1533661924]。
当前线程[thread-t1]执行结束。
由输出结果可知,程序可以正常运行。这是因为线程t1和线程t2的run方法中同步代码块中的锁对象都是t1对象,在线程t2的run中调用了t1.join()方法,由上面分析可知,在join方法内部,线程t2会再次获取t1对象作为锁对象进入到join方法内部的同步方法中,并在执行join方法内部的wait方法时释放掉t1对象锁,这时线程t2就不再拥有任何锁,线程t1就可以获取到锁对象t1从而执行自己的run方法。
如何调用wait方法?在if语句块中还是在循环语句中?为什么?
答:在while循环语句中使用wait方法。因为当某些条件不满足时,在当前线程的同步方法或同步块中调用锁对象的wait方法,使得线程等待其他条件满足,所以在处理前,使用循环检测条件是否满足。
synchronized (obj) {while (条件不满足)obj.wait(); // 释放锁,等待被唤醒... // 满足条件,继续执行
}
Java线程中的Thread.yield()方法(Thread类中的native静态方法),译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
yield()的作用是让步。它能让当前线程由“正在运行状态”(实际上这不是单独存在的状态,而是包含在“可运行状态”中)进入到“可运行状态”,从而让其它具有相同或更高优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程再次获得CPU执行权,又进入到“正在运行状态”继续运行!