Android 等待唤醒机制

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将其挂起。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值