线程等待、唤醒、休眠和中断(5)

前言

本章内容涉及wait()、notify()、notifyAll()、sleep()、join()、interrupt()和对应的超时方法。

Object中的相关方法介绍

package java.lang;

public class Object {
    //唤醒此对象监视器等待的单个线程,被唤醒线程进入就绪状态
    public final native void notify();
    //唤醒此对象监视器等待的所有线程,被唤醒线程进入就绪状态
    public final native void notifyAll();
    //让当前线程进入等待(阻塞)状态,超时会被唤醒
    public final native void wait(long timeout) throws InterruptedException;
    //纳秒大于零,毫秒数加1。如果有需求,直接把毫秒数加1调用上一个方法即可
    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);
    }
    //让当前线程进入无限期等待(阻塞)状态
    public final void wait() throws InterruptedException {
        wait(0);
    }
    ...
}

注释中已经大致说明了方法用途。

wait()、notify()

线程的等待与唤醒为什么在Object中而不在线程Thread中,需要强调的是,这里说的线程等待是指让线程等待在某一个对象的监视器上(用Object.wait()表示),等待时会释放持有该对象的同步锁,依赖于synchronized关键字使用(否则报监视器状态异常IllegalMonitorStateException)。同样,线程唤醒也是指唤醒等待在某一个对象监视器上的线程(用Object.notify()表示),也依赖于synchronized关键字。说到这里,前面提出的问题已经基本回答完了,说白了,线程的等待与唤醒都是基于某一对象的监视器,而线程本身只是其中的一种而已。代码示例:

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author HXJ
 * @date 2018/7/20
 */
public class WaitNotify {
    public static void main(String[] args) {
        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //用于等待唤醒的对象
        final Object waitAndNotify = new Object();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " notify线程开始运行!...");
                //thread-notify线程在main线程等待释放同步锁之前阻塞在waitAndNotify对象的监视器上
                synchronized (waitAndNotify) {
                    System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 唤醒主线程!");
                    //唤醒在waitAndNotify对象监视器上等待的main线程
                    waitAndNotify.notify();
                    System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 已唤醒主线程!");
                }
            }
        }, "thread-notify");

        synchronized (waitAndNotify) {
            System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " start notify线程");
            thread.start();
            try {
                //主线程休眠两秒是为了表现thread-notify线程的阻塞等待
                Thread.sleep(2 * 1000);
                System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主线程!wait()");
                //让线程等待在waitAndNotify对象的监视器上,并释放同步锁
                waitAndNotify.wait();
            } catch (InterruptedException e) {
            }
            System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主线程!继续运行...");
        }

    }
}

运行结果:

2018-07-20 17:53:06 main start notify线程
2018-07-20 17:53:06 thread-notify notify线程开始运行!...
2018-07-20 17:53:08 main 主线程!wait()
2018-07-20 17:53:08 thread-notify 唤醒主线程!
2018-07-20 17:53:08 thread-notify 已唤醒主线程!
2018-07-20 17:53:08 main 主线程!继续运行...

运行结果说明:
从代码中可以看出,主线程main先创建了一个用于等待、唤醒的对象waitAndNotify,然后再创建线程thread-notify,线程thread-notify的运行逻辑用于唤醒等待线程。接着主线程main获取waitAndNotify对象同步锁,启动线程thread-notify;由于waitAndNotify同步锁已经被main线程持有,且休眠两秒,所以线程thread-notify阻塞等待waitAndNotify对象同步锁,直到main线程休眠结束后调用wait()等待在waitAndNotify对象监视器上并释放同步锁;接着线程thread-notify取得同步锁,唤醒等待在waitAndNotify监视器上的main线程,main线程继续运行至结束。

wait(long timeout)、notifyAll()

前边介绍完wait()和notify()后,这两个函数已经很简单了,通过Object源码发现wait()也是通过调用wait(long timeout)实现的,参数为0意思是无限期等待,直到被唤醒。如果参数大于0,假如参数是500,意思是等待500毫秒,等待期间如果线程未被唤醒,则500毫秒后自动唤醒。notify()是唤醒一个等待线程,而notifyAll()是唤醒所以等待的线程。用法与notify()一致。

Thread中的sleep(long millis)、join()、interrupt()

sleep(long millis)

java.lang.Thread

    /**
     * 线程休眠,定义抛出中断异常
     */
    public static native void sleep(long millis) throws InterruptedException;

线程休眠,Thread中的静态方法,用法比较简单,上文代码示例中已经出现过,Thread.sleep(毫秒数)即让当前运行的线程进入TIMED_WAITING (sleeping)阻塞状态,调用完成,当前线程进入休眠状态,直到休眠设置的毫秒数后由系统唤醒。需要注意的是线程与同步锁没有关系,所以不会存在等待释放同步锁一说,它可以随意的嵌入方法代码的任何地方进行调用。上文代码片段:

try {
    //主线程休眠两秒是为了表现thread-notify线程的阻塞等待
    Thread.sleep(2 * 1000);
    System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主线程!wait()");
    //让线程等待在waitAndNotify对象的监视器上,并释放同步锁
    waitAndNotify.wait();
} catch (InterruptedException e) {
}
jion()

jion()线程间等待,实际上是通过Object.wait()实现的。
java.lang.Thread

    /**
     * 等待这个线程运行结束
     */
    public final void join() throws InterruptedException {
        join(0);
    }

    /**
     * 大于等于500000纳秒或纳秒大于0且毫秒等于零时,毫秒加一
     */
    public final synchronized void join(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++;
        }

        join(millis);
    }

    /**
     * 等待指定的毫秒数后,如果被等待的线程还没结束会超时自动唤醒,放弃等待
     * 注意synchronized关键字,说明获取的是这个线程实例对象的同步锁,等待在这个线程实例的监视器上
     */
    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) {
            //参数为0,如果this线程实例(非当前执行调用线程)还活着,则等待无限期等待,直到被等待线程运行结束
            while (isAlive()) {
                wait(0);
            }
        } else {
            //超时等待,等待当前线程实例运行结束或超时后系统唤醒
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从代码中可以看出来,join()方法是让调用它的线程进行等待,等待在某一个线程实例的监视器上,说白了就是调用哪个线程实例的join()方法,就是等待该线程运行结束,或是等待超时后等待的线程继续运行。不知道大家有没有发现,Thread中的join()方法中的wait()并没有对应的notify(),被等待的线程运行结束后是怎么唤醒等待它的线程呢?其实线程运行结束退出时,jvm会执行退出线程的本地退出exit方法,执行退出逻辑。Thread.cpp中相应代码:

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  java_lang_Thread::set_thread(threadObj(), NULL);

  //此处为唤醒等待在此线程监视器上的所有线程
  lock.notify_all(thread);

  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}


// 线程退出函数
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
  ...

  // Notify waiters on thread object. This has to be done after exit() is called
  // on the thread (if the thread is the last thread in a daemon ThreadGroup the
  // group should have the destroyed bit set before waiters are notified).
  //这里从命名可以看出,线程退出确保处理join相关逻辑
  ensure_join(this);
  
  ...
    
  // Remove from list of active threads list, 
  //and notify VM thread if we are the last non-daemon thread
  Threads::remove(this);
}

代码中可以看出,线程退出逻辑中有唤醒所有等待线程的相关逻辑。
主线程等待子线程 代码示例:

public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {
        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 2; i++) {
                    try {
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 运行中...");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 运行结束");
            }
        }, "thread-sub");
        //启动子线程
        thread.start();
        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 等待子线程运行结束...");
        //等待子线程运行结束,即主线程等待在子线程实例的监视器上
        thread.join();
        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 继续运行...");
    }
}

运行结果:

2018-07-24 16:38:56 main 等待子线程运行结束...
2018-07-24 16:38:56 thread-sub 运行中...
2018-07-24 16:38:57 thread-sub 运行中...
2018-07-24 16:38:58 thread-sub 运行结束
2018-07-24 16:38:58 main 继续运行...

从结果可以看出,主线程会等待子线程运行结束后才会继续运行。从实例代码中看,可能你会疑问,main执行子线程thread.join()调用,为什么进行等待的是main而不是thread-sub子线程呢?我们再回头看join(long millis)源码,源码中isAlive()方法在本示例代码中是判断thread-sub子线程如果还没有运行结束,在正在运行的main主线程调用wait(0)或wait(delay)进行等待。再次强调,wait()作用是让当前线程等待,也就是让main主线程等待在thread-sub子线程对象的监视器上,thread-sub子线程运行结束后再唤醒等待在自己监视器上的所有线程。
最后提个问题,如果是让主线程等待多个子线程呢?有怎么实现...

interrupt()

在《Core Java》中有这样一句话:“没有任何语言方面的需求要求一个被中断的程序应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断 ”
一个现在未正常结束之前,被强制终止是很危险的事情,比如终止了一个持有锁的线程,那么有可能所有等待锁的线程都将永久阻塞。Thread中的Thread.suspend线程挂起, Thread.stop线程止等方法都被标记Deprecated弃用了。
但有时候我们确实有必要终止某一个线程,该怎么做呢,优雅的方式便是线程中断,这里要强调的是,线程中断并非线程终止,而是要给该线程发一个中断信号让它自己决定如何处理,调用某个线程interrupt()方法,会设置该线程的中断状态标识来通知它被中断了。运行中的线程只是设置了中断状态,isInterrupted()返回true;对于阻塞中的线程,收到中断信号后会产生一个中断异常InterruptedException,同时清除中断状态重新复位为false,打破阻塞状态,相当于唤醒阻塞(通过wait()、sleep()、InterruptibleChannel I/O操作、Selector阻塞)线程。并非所有阻塞状态的线程都能对中断有响应,如中断信号并不能使BLOCKED (on object monitor)状态的死锁线程恢复运行。且看源码:
java源码比较简单:

    /**
     * Interrupts this thread.
     */
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

本地方法c++源码:

thread.cpp
void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}
//windows实现,其他系统应该是一样的逻辑
os_windows.cpp
void os::interrupt(Thread* thread) {
  assert(!thread->is_Java_thread() || Thread::current() == thread || Threads_lock->owned_by_self(),
         "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
   //设置中断标识
  osthread->set_interrupted(true);
  // More than one thread can get here with the same value of osthread,
  // resulting in multiple notifications.  We do, however, want the store
  // to interrupted() to be visible to other threads before we post
  // the interrupt event.
  OrderAccess::release();
  SetEvent(osthread->interrupt_event());
  // For JSR166:  unpark after setting status
  //设置中断标识后unpark()唤醒线程
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}

源码中有两行关键的代码
osthread->set_interrupted(true); 设置中断标识
if (thread->is_Java_thread()) ((JavaThread*)thread)->parker()->unpark(); //唤醒阻塞线程
java中调用interrupt()方法后,并不能中断一个正在运行的线程。实际上是设置了线程的中断标志位,在线程阻塞的地方(如调用sleep、wait、join等地方)抛出一个异常InterruptedException,并且中断状态也将被清除,重新复位为false,这样线程就得以退出阻塞的状态。
经典代码示例:

  public void run() {
      try {
          ....
          while (!Thread.currentThread().isInterrupted() && more work to do){
              // do more work;
          }
      } catch (InterruptedException e) {
          // thread was interrupted during sleep or wait
      } finally {
          // cleanup, if required
      }
  }

代码中,线程不停的检查自身的中断状态作为while循环的条件,当线程的Thread.interrupt方法被其他线程调用,中断状态被设置为true时,退出循环正常结束运行。这说明,interrupt中断只是线程退出逻辑的一部分,前提是线程需要通过isInterrupted()检查自己的中断状态

中断阻塞状态

对于阻塞状态(常见的通过wiat、sleep等方法进行阻塞,这些能抛出InterruptedException异常)的线程,中断信号会产生一个中断异常,使线程从阻塞状态中恢复运行,换句话说就是阻塞被中断了,线程被唤醒了。抛出InterruptedException中断异常后,线程中断状态复位false。中断异常也是线程退出逻辑的一部分。

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author HXJ
 * @date 2018/7/26
 */
public class ThreadInterrupt {
    public static void main(String[] args) {
        final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //检查自身中断状态
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep ...");
                        Thread.sleep(10 * 1000);
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 结束");
                    } catch (InterruptedException e) {
                        //InterruptedException中断异常会复位中断状态为false
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 产生中断异常!");
                    }
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //检查自身中断状态
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep ...");
                        Thread.sleep(10 * 1000);
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 结束");
                    } catch (InterruptedException e) {
                        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 产生中断异常!");
                        //InterruptedException中断异常会复位中断状态为false,所以需要重新设置中断状态
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 正常退出!");
            }
        });
        System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 启动子线程!");
        thread.start();
        thread1.start();
        try {
            Thread.sleep(2000);
            System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 2s 结束,中断阻塞子线程!");
            thread.interrupt();
            thread1.interrupt();
            thread.join();
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

2018-07-26 14:59:44 main 启动子线程!
2018-07-26 14:59:44 Thread-0 sleep ...
2018-07-26 14:59:44 Thread-1 sleep ...
2018-07-26 14:59:46 main sleep 2s 结束,中断阻塞子线程!
2018-07-26 14:59:46 Thread-1 产生中断异常!
2018-07-26 14:59:46 Thread-0 产生中断异常!
2018-07-26 14:59:46 Thread-1 正常退出!
2018-07-26 14:59:46 Thread-0 sleep ...
2018-07-26 14:59:56 Thread-0 sleep 结束
2018-07-26 14:59:56 Thread-0 sleep ...

结果说明,主线程启动thread、thread1两个子线程后,开始休眠;两个子线程启动后也进入长达10s的休眠状态;主线程2s后休眠结束,分别中断了休眠阻塞状态的两个子线程,两个子线程产生中断异常恢复运行,提前结束休眠状态。由于产生中断异常后中断状态复位,所以Thread-0子线程的while条件isInterrupted()仍满足条件继续执行;而Thread-1在中断状态复位后interrupt()重新设置中断状态,while条件不满足,线程正常退出。产生中断异常后,也可以通过break直接退出循环体。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,436评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,948评论 1 18
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,709评论 12 45
  • 当夏日的风拂过我的脸庞,当我看见耀眼的阳光,当我怀着浮萍一般的心,当我还在等你。 等你,成为了我每日期待的事情...
    乔百合阅读 240评论 0 0
  • 在前面的回忆中,更多的谈的都是父母的影响,其实,家里还有一个重要成员,那就是我的弟弟。 弟弟比我小两岁。听爸爸妈妈...
    macaho阅读 196评论 0 0