1.等待唤醒机制
等待唤醒机制是一种线程间协作的机制,用于实现线程之间的通信和同步。
等待唤醒机制通常用于多线程环境下,其中一个线程等待某个条件得到满足,而另一个线程负责在满足条件时通知等待的线程继续执行。
多个线程在处理同一个资源时,为了避免对同一共享变量的争夺,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。这时候就需要用到等待唤醒机制。
①wait():当前线程立即释放锁,进入等待状态,该方法会将当前线程放入wait set队列里,并且在wait()所在的代码处停止执行,直到其他线程通过notify或notifyAll方法来唤醒它。被唤醒的线程将重新尝试获取锁,然后继续执行。
wait()方法会释放锁,因此不会浪费CPU资源,也不会去竞争锁,这时的线程状态是WAITING。即一个线程因为执行操作所需的保护条件未满足而被暂停,需要等着其他线程执行notify通知操作将这个等待的线程从wait set中释放出来,重新进入到调度队列中。
②notify():用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则随机挑选出其中一个等待状态的线程,对其发出通知notify进行唤醒,并使它有机会获取该对象的对象锁。
选取wait set中的一个线程释放。即当前线程更新了共享变量,使得其他线程需要的保护条件成立,唤醒了被暂停的线程。
注意,执行notify()方法后当前线程不会立即释放其拥有的对象锁,而是执行完之后才会释放该对象锁,被通知的线程也不会立即获得对象锁,而是等待notify()方法执行完之后,释放了该对象锁,才可以获得该对象锁。
③notifyAll():通知所有等待同一共享资源的全部线程从等待状态退出,进入可运行状态,重新竞争获得对象锁,即释放wait set里的全部线程。
notifyAll()唤醒所有处于等待状态的线程,并不是给所有唤醒的线程一个对象锁(每个对象只有一个锁),而是让它们竞争,当其中一个线程运行完就开始运行下一个已经被唤醒的线程,因为锁已经转移了。
开发中应该尽量使用notifyAll(),因为notify()非常容易导致死锁。当然notifyAll并不一定都是优点,毕竟一次性将等待队列中的所有线程都唤醒是非常耗性能的。
等待唤醒机制提供了一种有效的线程间协作方式,使得线程可以根据特定条件进行等待和唤醒,从而实现对共享资源的更好利用和管理。
执行wait()方法的线程叫等待线程,执行notify()方法的线程叫通知线程。调用wait()方法会使当前线程A马上失去对象锁并且沉睡,直到有其他线程B调用notify()唤醒该线程,此时持有对象锁的线程B会先行执行完毕然后再将对象锁交给线程A继续执行(即调用wait会立即释放锁,而调用notify不会释放锁,线程会一直执行完毕才释放锁)。
注意:等待唤醒机制必须在同步代码块中执行。即wait、notify、notifyAll必须在synchronized中执行,否则会抛出异常(原因:在wait()方法的native代码中,会判断线程是否持有当前对象的内部锁,如果没有的话,就会报非法监视器状态异常)。因此在调用wait()、notify()、notifyAll()之前,线程必须先获得相应对象的锁。
2.wait/notify示例
①使用Object作为对象锁
public class TestActivity extendsActivity {
private static Object lockObject = new Object();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.activity_main);
System.out.println("主线程运行");
Thread thread = new WaitThread();
thread.start();
long start = System.currentTimeMillis();
synchronized (lockObject) {
try {
System.out.println("主线程等待");
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "主线程继续 --> 等待的时间:" + (System.currentTimeMillis() - start));
}
}
class WaitThread extends Thread {
@Override
public void run() {
synchronized (lockObject) {
try {
Thread.sleep(2000); //子线程等待2秒后唤醒lockObject锁
lockObject.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
主线程和子线程使用的是同一个对象锁lockObject,并且用同一个对象lockObject执行的wait()和notify()方法,因此保证了线程同步。
当主线程执行到wait()方法时,主线程会释放对象锁,同时主线程会被加到【等待唤醒队列】中进行等待;这时子线程获得该对象锁开始执行同步代码块,子线程运行两秒钟后执行notify()方法就会唤醒【等待唤醒队列】中的一个线程,这里就是主线程;如果使用notifyAll()方法则会唤醒整个【等待唤醒队列】中的所有线程。
②使用自定义的Bean类作为对象锁
除了可以使用Object作为对象锁,也可以使用自定义的Bean类作为对象锁。
注意:调用wait和notify的对象不能是基本类型,应该为可引用类型或者javabean。
举例:有一个供线程竞争的Person类,两个线程并发执行。
final Person person = new Person("张三",23);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (person){
try {
for (int i = 0 ; i<5 ; i++) {
Thread.sleep(1000);
Log.e(TAG,"thread1 "+i);
if (i==2){
person.wait();
}
}
} catch (InterruptedException e) {
}
}
}
},"Thread1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (person){
try {
for ( int i = 0 ; i < 5 ; i++ ){
Thread.sleep(1000);
Log.e(TAG,"thread2 "+i);
if (i==2){
person.notify();
}
}
} catch (InterruptedException e) {
}
}
}
},"Thread2");
t1.start();
t2.start();
线程1和线程2是并发执行的。线程1每隔1s打印一条日志。当循环到第2次的时候会调用wait()方法放弃对象锁并沉睡。此时线程2获得对象锁开始执行。执行到第2次循环的时候会调用notify()方法唤醒线程1。
注意:线程2调用notify的时候,线程2正持有锁,因此虽然线程1被唤醒,但是仍无法获得obj锁。直到线程2退出synchronized代码块,释放obj锁后,线程1才有机会获得锁继续执行。
执行结果:
15435-15906 thread1 0
15435-15906 thread11
15435-15906 thread1 2
15435-15907 thread2 0
15435-15907 thread2 1
15435-15907 thread2 2
15435-15907 thread2 3
15435-15907 thread2 4
15435-15906 thread1 3
15435-15906 thread1 4
3.wait/notify原理
JVM给每个使用synchronized的对象都维护了一个入口集Entry Set和一个等待集Wait Set。
入口集用于存储申请该对象内部锁的线程。如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。
等待集用于存储对象上的等待线程。如果线程A调用了wait()方法,那么线程A会释放该对象锁进入到Wait Set,并且处于线程的WAITING状态。
如果某个线程B想要获得对象锁,一般情况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等),二是线程B已处于RUNNABLE状态。
那么入口集EntrySet和等待集WaitSet中的线程都在什么条件下可以转变为RUNNABLE呢?
对于入口集Entry Set中的线程,当对象锁被其他线程释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。
对于等待集Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某个线程,这个线程的状态就从WAITING转变为RUNNABLE或者当notifyAll()方法被调用时Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。然后每当对象锁被释放后,所有处于RUNNABLE状态的线程会共同去竞争获取对象锁,最终只会有一个线程(具体哪一个取决于JVM实现)真正获取到对象锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。
wait()方法会将当前线程暂停,在释放内部锁时,会将当前线程存入该方法所属的对象等待集中。
调用对象的notify()方法会让该对象的等待集中的任意一个线程唤醒,被唤醒的线程会继续留在对象的等待集中,直到该线程再次持有对应的内部锁时,wait()方法就会把当前线程从对象的等待集中移除。
添加当前线程到等待集、暂停当前线程、释放锁以及把唤醒后的线程从对象的等待集中移除,都是在wait()方法中实现的。
在wait()方法的native代码中,会判断线程是否持有当前对象的内部锁,如果没有的话就会报非法监视器状态异常,这也就是为什么要在同步代码块中执行wait()方法。
4.其他使线程等待的方法
1)Thread.sleep()
sleep()和wait()方法都会让当前线程进入阻塞状态。
sleep是Thread类中的静态方法,它让线程休息指定的时间,时间一到就继续运行。sleep不会导致锁行为的改变。如果当前线程拥有锁,那么sleep()不会使当前线程释放锁,它仅仅是释放cpu资源,使当前线程进入睡眠状态,当睡眠状态结束后,当前线程就会进入就绪状态准备和其他线程竞争cpu资源。
而调用wait方法会把当前线程的对象锁释放掉,并进入阻塞状态,需要在别的线程中调用notify或者notifyAll重新唤醒阻塞的线程。
sleep和wait的区别:
①来自的类不同,sleep方法来自Thread,wait方法来自Object;
②sleep方法是线程内部方法,不会释放对象的锁,而wait方法会释放对象锁,使得其他线程可以使用同步控制块或者方法;
③wait/notify/notifyAll是对象操作方法,必须在同步下进行,只能在synchronized里面使用,而sleep可以在任何地方使用。
④两者都可以让线程暂停一段时间,本质的区别是一个是线程的运行状态控制,一个是线程之间通讯的问题,需要激活才会进入runing状态。
2)join()
Thread.join()方法可以让一个线程等待另一个线程执行结束后再继续执行。
public class Test {
public static void main(String[] args) {
ThreadTest t1=new ThreadTest("A");
ThreadTest t2=new ThreadTest("B");
t1.start();
t1.join();
t2.start();
}
}
class ThreadTest extends Thread{
private String name;
public ThreadTest(String name){
this.name = name;
}
public void run(){
for(int i=1;i<=5;i++){
Log.e(TAG, name+"-"+i);
}
}
}
运行结果:
A-1
A-2
A-3
A-4
A-5
B-1
B-2
B-3
B-4
B-5
使用t1.join()之后,B线程需要等A线程执行完毕之后才能执行。需要注意的是t1.join()需要等t1.start()执行之后执行才有效果,此外如果t1.join()放在t2.start()之后的话,仍然会交替执行,并不是没有效果。
其实join()方法实现等待就是通过wait()实现的。在join()方法中会不断判断调用了join()方法的线程是否还存活,是的话则继续等待。
public final void join() throws InterruptedException {
join(0); //join()等同于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()) {
wait(0); //join(0)等同于wait(0),即wait无限时间直到被notify
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join()方法的底层是利用了wait()方法实现。由此,join方法是一个同步方法,当主线程调用t1.join方法时,主线程先获得了t1对象的锁,随后进入join()方法调用t1对象的wait(),使主线程进入了t1对象的等待池,此时A线程还在执行,并且随后的t2.start()还没被执行,因此B线程还没开始。等到A线程执行完毕之后,主线程继续执行,走到了t2.start(),B线程才会开始执行。
3)yield()
yield()的作用是停止当前线程,让同等优先权线程先运行,如果没有同等优先权的线程,那么yield()方法将不起作用。
①yield()会礼让给相同优先级的或者是优先级更高的线程执行,但是yield()方法只是把当前线程的执行状态打回准备就绪状态,所以执行了该方法后,有可能马上又开始运行,有可能等待很长时间。
②yield()使正在执行的线程暂停,释放自己拥有的CPU,进入抢占CPU队列,但不会阻塞线程。
③yield()不会释放锁。
public class YieldActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_yield);
MyThread myThread1 = new MyThread("线程一");
MyThread myThread2 = new MyThread("线程二");
myThread1.start();
myThread2.start();
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public synchronized void run() {
for (int i = 0; i < 100; i++) {
Log.e(TAG, getName() + "在运行,i的值为:" + i + " 优先级为:" + getPriority());
if (i == 2) {
Log.e(TAG, getName() + "礼让");
Thread.yield();
Thread.sleep(1000);
}
}
}
}
}
通过Thread.sleep()的方式让线程强行延迟一秒回到准备就绪状态,这样在打印信息上就能看到想要的结果了:
线程二在运行,i的值为:0 优先级为:5
线程二在运行,i的值为:1 优先级为:5
线程二在运行,i的值为:2 优先级为:5
线程二礼让
线程一在运行,i的值为:0 优先级为:5
线程一在运行,i的值为:1 优先级为:5
线程一在运行,i的值为:2 优先级为:5
线程一礼让
线程二在运行,i的值为:3 优先级为:5
线程二在运行,i的值为:4 优先级为:5
线程二在运行,i的值为:5 优先级为:5
线程二在运行,i的值为:6 优先级为:5
....
4)Condition.await/signal
如果程序不使用synchronized来保证线程同步,而是直接使用Lock对象,那么就不能使用wait()、notify()、notifyAll()方法进行线程间通信了,此时java提供了Condition类来让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程。
Condition实例被绑定到一个Lock对象上,要获取特定Lock对象的Condition实例,只需要调用Lock对象的newCondition()方法即可。
Condition底层本质上是park/unpark实现阻塞。
Condition类提供了如下三个方法。
①await():类似于synchronized的wait()方法,使当前线程等待,直到其他线程调用该Condition的signal()或signalAll()方法来唤醒该线程。
await()方法有很多变体,如:long awaitNanos( long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的操作。
②signal():唤醒此Lock对象上等待的单个线程。如果所有的线程都在Lock上等待,则会唤醒其中的一个,唤醒具有任意性。只有当前线程放弃对该Lock对象的锁定后(使用await()方法)才可以执行唤醒操作。
③signalAll():唤醒此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后(使用await()方法)才可以执行唤醒操作。
示例:
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private volatile boolean conditionSatisfied = false;
private void startWait() {
lock.lock();
Log.e(TAG, "等待线程获取了锁");
try {
while (!conditionSatisfied) {
Log.e(TAG,"保护条件不成立,等待线程进入等待状态");
condition.await();
}
Log.e(TAG,"等待线程被唤醒,开始执行目标动作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
Log.e(TAG,"等待线程释放了锁");
}
}
public void startNotify() {
lock.lock();
Log.e(TAG, "通知线程获取了锁");
try {
conditionSatisfied = true;
Log.e(TAG,"通知线程即将唤醒等待线程");
condition.signal();
} finally {
Log.e(TAG,"通知线程释放了锁");
lock.unlock();
}
}
在两个线程中分别执行上面的两个函数后,能得到下面的输出:
等待线程获取了锁
保护条件不成立,等待线程进入等待状态
通知线程获取了锁
通知线程即将唤醒等待线程
通知线程释放了锁
等待线程被唤醒,开始执行目标动作
等待线程释放了锁
注:使用while是防止处于WAITING状态下线程被别的原因调用了唤醒(notify或notifyAll)方法,但是while里面的条件并没有满足(也可能当时满足了,但是由于别的线程操作后,又不满足了),就需要再次调用wait将其挂起。