线程通知与等待

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包括了线程的通知和等待。

wait以及notify

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起。直到发生下面几件事情之一才返回。

  • 其他线程调用了该共享对象的notify()或者notifyAll()。
  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外,需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()时调用线程会抛出IllegalMonitorStateException异常。

一个线程如何获取一个共享变量的监视器锁

  • 方法1:执行sychronized同步代码块时,使用该共享变量作为参数
 sychronized(该共享变量){
 }
  • 方法2:调用该共享变量的方法,并且该方法使用了sychronized修饰。
 sychronized void method{
 }

虚假唤醒:一个线程即使没有被其他线程调用notify()、notifyAll()进行通知或者被中断,等待超时也可以从挂起状态变为可以运行状态,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待。也就是说在一个循环中调用wait()方法进行防范,退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj) {
 while (条件不满足){
  obj.wait() ;
 }
}

实例

其中queue为共享变量,生产者线程在调用queue的wait()方法之前,使用sychronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。

//生产线程
synchronized (queue) {
//消费队列满,贝等待队列空闲
while (queue.size() == MAX SIZE) {
 try { 
  //挂起当前线程, 并释放通过同步块获取的 queue上的锁,上消费者线程可以获取该锁,然后获取队列里面的元素
   queue .wait() ;
  } catch (Exception ex) {
   ex.printStackTrace() ;
  }
}
// 空闲则生成元素,并通知消费者线程
 queue.add(e);
 queue.notifyAll();
}

//消费者线程
sychronized(queue){
  while(queue.size == 0){
   try{
     //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列
     queue.wait();
   }catch(Exception e){
     e.printStackTrace() ;
   }
  }
  //消费元素,并唤醒生产者线程
  queue.take();
  queue.notifyAll();
}

此外,当前线程调用共享变量的wait()方法后,只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁不会释放。

如下所示:

//创建共享资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
new Thread(()->{
 try{
   sychronized(resourceA){
      sychronized(resourceB){
      // 此处线程A只释放了获取到的resourceA的锁,resourceB的锁并为释放
       resourceA.wait();   
   }
  }
 }

}).start();

wait(long timeout)

该方法相比wait()多了一个超时参数,他的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()唤醒,该函数会因为超时而返回。如果将timeout设置为0,则不限制超时时间。

notify()

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,他仍然需要和其他线程一起竞争该锁,只有竞争到了共享变量的监视器锁后才可以继续执行。

notifyAll()

不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

等待线程执行终止的join方法

在主线程里面启动两个子线程,然后分别调用他们的join()方法,那么主线程首先会在调用theadOne.join()方法后被阻塞,等待theadOne执行完毕后返回。然后主线程调用threadTwo.join()方法后再次被阻塞,等待threadTwo执行完毕后返回。

yield()让出CPU执行权

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际上就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示

当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程。

线程中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止线程的执行,而是被中断的线程根据中断状态自行处理。

  • void interrupt(): 中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true 并立即返回。线程A实际并没有被中断,他会继续往下执行。如果线程A因为调用了wait、join、sleep方法而被阻塞挂起,这个时候如果线程B调用线程A的interrupt方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。

  • boolean isInterrupted()方法: 检测当前线程是否被中断,是返回true。

  • boolean interrupted() 方法检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted方法不同,该方法如果发现当前线程被中断,则会清除中断标志。

示例,当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait函数或者join函数来阻塞挂起当前线程。比如一个线程调用了Threa.sleep(3000), 那么调用线程会被阻塞3s后才会从阻塞状态变为激活状态。但是有时候,如果一直等到3s后再返回有点浪费时间。这时候可以 调用线程的interrupt()方法,强制sleep方法抛出InterruptedException异常而返回。线程恢复到活跃状态。

线程上下文切换

在多线程编程中,线程个数一般大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU给其他线程占用,这就是上下文切换。

从当前线程的上下文切换到其他线程,被切换到的线程需要知道上次CPU运行到了哪里,所以在切换线程的时候要保存好上下文。当再次执行时根据保存的现场信息恢复执行。

守护线程和用户线程

Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实,在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。

守护线程和用户线程有什么区别:区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,并不影响JVM的退出。

如何创建一个守护线程,代码如下

   Thread thread = new Thread(() -> {
   });
   //设置为守护线程
   thread.setDaemon(true);
   thread.start();

只需要设置线程的dameon参数为true即可。

对比实验

例一:

    public static void main(String[] args) {
        new Thread(() -> {
            for (; ; ) {
            }
        }).start();
        System.out.println("main Thread is over");
    }

结果表明,main线程运行结束,JVM进程并未退出,说明 当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。也说明了在用户线程还存在的情况下JVM进程并不会终止。

例二:

    public static void main(String[] args) {
       Thread thread1 =  new Thread(() -> {
            for (; ; ) {
          
            }
        });
        thread1.setDameon(true);
        thread1.start();
        System.out.println("main Thread is over");
   }

修改以后,发现当main线程运行结束以后,JVM进程也已经终止了,在这个例子中,main函数是唯一的用户线程,thread线程是守护线程,当main线程运行结束后,JVM发现当前已经没有用户线程了,就会终止JVM进程。由于这里的守护线程执行的时候是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则 JVM不等守护线程运行完毕就结束JVM进程

总结:非守护线程(也称为用户线程)都结束了,JVM就会终止,此时所有守护线程会被强制终止,它们正在执行的任务也会随之中断

main线程运行结束后,JVM会自动启动一个叫做DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。JVM代码如下:

    int JNICALL 
    JavaMain(void * args){
        //执行 Java中的 main函数
        (*env) ->CallStaticVoidMethod(env, mainClass, mainID, mainArgs) ; 
        //main函数返回值
        ret = (*env)->ExceptionOccured(env) == NULL ?0 :1;
        //等待所有非守护线程结束, 然后销毁JVM进程
        LEAVE();
    }

LEAVE()是C语言中的一个宏定义,作用是创建一个名为DestroyJavaVM的线程来等待所有用户线程执行结束。

总结:如果希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

补充:执行kill 信号,杀死一个应用程序时,如果应用程序仍然有未完成的任务,可以通过添加钩子函数的方式使其可以继续处理并优雅退出

public class App {
    public static void main(String[] args) {
        // 添加一个关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("正在执行清理操作...");
            // 这里放置清理资源的代码,例如关闭文件、数据库连接等
            performCleanup();
            System.out.println("清理完成。");
        }));

        System.out.println("应用程序正在运行...");
        // 模拟应用程序运行
        try {
            Thread.sleep(10000); // 假设应用运行了一段时间
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("线程被中断。");
        }
    }
    private static void performCleanup() {
        // 执行实际的清理操作,比如关闭资源等
        System.out.println("执行资源关闭等清理操作。");
    }
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部