Java多线程研究03-线程的基本操作(notify,notifyAll,interrupt,join,sleep)

Posted zczpeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程研究03-线程的基本操作(notify,notifyAll,interrupt,join,sleep)相关的知识,希望对你有一定的参考价值。

JAVA中的基本线程操作

这是前面给出的线程状态切换图例,可能还不能完全理解其中的切换条件,我们将详细介绍JAVA中如何进行这些线程状态的操作。
本文将讲解notify、notifyAll、interrupt、join和sleep等操作。

notifynotifyAll操作

notify方法的工作情况

通过代码解释:

package com.zczpeng.thread;

public class NotifyTheadTest 

    /**
     * @param args
     */
    public static void main(String[] args) 
        new Thread(new ParentThread()).start();
    


class ParentThread implements Runnable

    /**
     * 这个对象,为每个ChildThread对象所持有,模拟这个对象为所有线程对象进行独占的现象
     */
    public static final Object THREAD_LOCK = new Object();
    @Override
    public void run() 

        /**
         * 启动三个对THREAD_LOCK对象进行独立抢占的线程
         */
        for(int i = 0;i<3;i++)
            ChildThread cthread = new ChildThread();
            new Thread(cthread).start();
        

        /**
         * 在synchronized这行加断点,保证这部分最后执行。
         */
        synchronized (THREAD_LOCK) 
            THREAD_LOCK.notify();
        
    



class ChildThread implements Runnable

    @Override
    public void run() 

        Thread t = Thread.currentThread();
        long id = t.getId();
        System.out.println("线程"+id+"启动成功,等待执行...");
        synchronized (ParentThread.THREAD_LOCK) 
            try 
                ParentThread.THREAD_LOCK.wait();
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
        //说明线程被唤醒了
        System.out.println("线程"+id+"执行了.");
    

以上两段代码中,ParentThread 类负责创建三个ChildThread类的对象,每一个ChildThread类的实例对象都持有ParentThread .THREAD_LOCK 对象的“钥匙”,并通过wait方法退出ParentThread .THREAD_LOCK 对象的独占状态(但是不归还锁)
然后我们通过ParentThread 类中的ParentThread .THREAD_LOCK .notify()方法解除阻塞状态:

synchronized (THREAD_LOCK) 
        THREAD_LOCK.notify();

以上代码的执行效果如下所示:

实际上,我们只知道有三个ChildThread类的实例对象处于等待ParentThread .THREAD_LOCK对象的“锁芯”空闲;我们并不知道ParentThread .THREAD_LOCK.notify()方法会将ParentThread .THREAD_LOCK对象的“锁芯”(独占权)交给这三个线程的哪一个线程(这个决定过程是由操作系统完成的)。而且我们还知道,ParentThread .THREAD_LOCK.notify()方法只会唤醒等待ParentThread .THREAD_LOCK对象“锁芯”(独占权)的三个ChildThread类的实例对象中的一个。

notifyAll方法的工作情况

实际上理解了notify()方法的工作情况,就不难理解notifyAll()方法的工作情况了。接下来,同样是以上小节的代码,我们将ParentThread 类中的ParentThread .THREAD_LOCK.notify()方法,替换成ParentThread .THREAD_LOCK.notifyAll()方法。如下代码片段所示:

synchronized (THREAD_LOCK) 
            THREAD_LOCK.notifyAll();
        

然后我们观察代码的执行结果:

等待ParentThread .THREAD_LOCK对象锁的“锁芯”(独占权)的三个线程被依次唤醒(依次得到独占权)。

interrupt信号

interrupt,单词本身的含义是中断、终止、阻断。当某个线程收到这个信号(命令)的时候,会将自生的状态属性置为“interrupted”,但是线程本身并不会立刻终止。程序员需要根据这个状态属性,自行决定如何进行线程的下一步活动。

上图是文章中已出现线程状态变化图,我们已经知道线程从创建后可以处于多种不同状态:就绪(可运行)、运行中、阻塞(等待中)、死亡。并不是线程处于任何状态,都可以接收interrupt信号。如果在收到interrupt信号时,线程处于阻塞状态(wait()、wait(time)或者sleep引起的),那么线程将会抛出InterruptedException异常:

当Thread收到interrupt信号时,可能的两种结果:要么其线程对象中的isinterrupt属性被置为true;要么抛出InterruptedException异常。注意,如果抛出了InterruptedException异常,那么其isinterrupt属性不会被置为true。

代码示例
下面我们通过一段测试代码,来说明interrupt信号的工作情况:

public class InterruptProcessor 

    public static void main(String[] args) throws Exception 
        // thread one线程
        Thread threadOne = new Thread(new Runnable() 
            @Override
            public void run() 
                Thread currentThread = Thread.currentThread();
                while(!currentThread.isInterrupted()) 
                    /*
                     * 这里打印一句话,说明循环一直在运行
                     * */
                    System.out.println("Thread One 一直在运行!");
                

                System.out.println("thread one 正常结束!" + currentThread.isInterrupted());
            
        );

        // thread two线程
        Thread threadTwo = new Thread(new Runnable() 
            @Override
            public void run() 
                Thread currentThread = Thread.currentThread();
                while(!currentThread.isInterrupted()) 
                    synchronized (currentThread) 
                        try 
                            // 通过wait进入阻塞 
                            currentThread.wait();
                         catch (InterruptedException e) 
                            e.printStackTrace(System.out);
                            System.out.println("thread two 由于中断信号,异常结束!" + currentThread.isInterrupted());
                            return;
                        
                    
                

                System.out.println("thread two 正常结束!");
            
        );

        threadOne.start();
        threadTwo.start();
        // 可以在这里打上端点,以保证threadOne和threadTwo完成了启动
        System.out.println("两个线程正常运行,现在开始发出中断信号");
        threadOne.interrupt();
        threadTwo.interrupt();
    

上面的示例代码中,我们创建了两个线程threadOne和threadTwo。其中threadOne线程在没有任何阻塞的情况下一直循环运行(虽然这种方式在正式环境中不建议使用,但是这里我们是为了模拟这种线程运行状态),每循环一次都检测该线程的isInterrupt属性的值,如果发现值为true则终止循环;另一个threadTwo线程,在启动后马上进入阻塞状态,等待唤醒(实际上没有其他线程会唤醒它,以便模拟线程阻塞的状态)。

这时我们向两个线程发出interrupt信号。以下是两个线程的执行结果:

Thread One 一直在运行!
Thread One 一直在运行!
thread one 正常结束!true
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:503)
at test.thread.interrupt.InterruptProcessor$2.run(InterruptProcessor.java:34)
at java.lang.Thread.run(Unknown Source)
thread two 由于中断信号,异常结束!false

通过显示的结果,我们看到threadOne线程的isInterrupt属性被成功置为true,循环正常结束线程运行正常完成;而threadTwo线程由于处于wait()引起的阻塞状态,所以在收到interrupt信号后,抛出了异常,其isInterrupt属性依然是false;


join操作

join操作会使两个线程的执行过程具有先后顺序,具体来说:如果A线程调用B线程的join操作,A线程就会一直等待(或者等待某个指定的时间长度),直到B线程执行完成后(死亡状态)A线程才会继续执行。如下图所示:

下面我们演示一段示例代码,在代码中我们创建了两个线程:一个是main方法执行的线程(记为mainThread),另一个是名叫joinThread的线程。接下来我们在mainThread中调用joinThread的join方法,让mainThread线程一直阻塞到joinThread线程执行结束后,再继续执行。

package com.zczpeng.thread;

public class JoinThread implements Runnable 

    public static void main(String[] args) 
        Thread thread = Thread.currentThread();
        long id = thread.getId();
        System.out.println("主线程" + id + "正在运行");
        Thread joinThread = new Thread(new JoinThread());
        joinThread.start();
        try 
            //如果没有这一行,则运行结果可能为结果1,有这一行,结果一定为结果2
            joinThread.join();
            System.out.println("主线程" + id + "运行結束");
         catch (InterruptedException e) 
            e.printStackTrace();
        

    

    @Override
    public void run() 
        Thread currentThread = Thread.currentThread();
        long id = currentThread.getId();
        System.out.println("线程" + id + "启动成功,准备进入等待状态(5秒)");

        // 使用sleep方法,模型这个线程执行业务代码的过程
        try 
            Thread.sleep(5000);
         catch (InterruptedException e) 
            System.out.println(e.getMessage());
        

        // 执行到这里,说明线程被唤醒了
        System.out.println("线程" + id + "执行完成!");
    

以下是执行结果:
如果不加 join,运行结果1:

但是加上join后的运行结果2:

注意:调用join方法的线程,如果接收到interrupt信号,也会抛出InterruptedException异常。

join()、join(millis)和join(millis, nanos)

在Java的基本线程操作中,join()、join(millis)和join(millis, nanos),都可以实现针对目标线程的等待,而这三个方法的区别主要是在等待时间上:
join:相当于调用join(0),即一直等待到目标线程运行结束,当前调用线程才能继续执行;

join(millis, nanos):调用线程等待 millis毫秒 + nanos 纳秒 时间后,无论无论目标线程执行是否完成,当前调用线程都会继续执行;实际上这个join方法的描述并不准确:第二个参数nanos只是一个参考值(修正值),且只有大于等于500000时,第二个参数才会起作用(纳秒是一秒的十亿分之一)。请看这个方法的源代码:

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);

以上是JDK1.7中,关于join(millis, nanos)方法的源代码。从源代码中我们可以看出,只有当nanos大于等于500000纳秒时,这个参数才发生意义。并且它的作用也并不是让调用线程再精确的等待如参数指定的纳秒数。


sleep操作

sleep将“当前线程”进入阻塞状态,并且不会释放这个线程所占用的任何对象锁的独占状态。这里请注意:
● 当前线程,是指当前调用sleep方法的线程。而不是指被调用的目标线程。请看如下代码片段:

Thread currentThread = Thread.currentThread();
Thread joinThread = new Thread(new SleepThread());
joinThread.start();

joinThread.sleep(50000);

请问,并阻塞的线程是哪一个?currentThread还是joinThread?思考1秒钟,,,那些心里回答“joinThread”的同学,说明还是没有搞清楚sleep方法的含义:请一定注意“当前线程”,进入阻塞状态的一定是“currentThread”。这也是为什么sleep方法是静态方法。

● sleep方法也不像wait方法的执行效果那样:sleep方法不会释放当前线程所占用的对象锁独占模式。请再看如下代码片段:

public class Join2Thread implements Runnable 



    public static void main(String[] args) throws Exception 
        Thread joinThread1 = new Thread(new Join2Thread());
        joinThread1.start();
        Thread joinThread2 = new Thread(new Join2Thread());
        joinThread2.start();
    

    @Override
    public void run() 
        // 使用sleep方法,模型这个线程执行业务代码的过程
        try 
            synchronized (Join2Thread.class) 
                Thread.sleep(Long.MAX_VALUE);
            
         catch (InterruptedException e) 
            LOGGER.error(e.getMessage(), e);
        
    

请问有几个线程可以获取Join2Thread.class对象锁的钥匙(独占Join2Thread.class对象锁的抢占权)?

当joinThread1进行对象锁检查的时候,发现Join2Thread.class对象没有其它任何线程独占,于是获得Join2Thread.class对象锁的独占权,并执行到“Long.MAX_VALUE”;由于sleep并不会释放这个joinThread1线程的任何对象锁独占权(肯定就不会释放Join2Thread.class对象锁的独占权);

那么当joinThread2线程进行Join2Thread.class对象锁独占权的检查是,发现它依然被joinThread1线程占有。所以joinThread2线程等待在边界区,无法获得Join2Thread.class对象锁的钥匙。
所以能够拥有Join2Thread.class对象锁钥匙的线程就只有一个:joinThread1。

总结

本文介绍了JAVA中线程的基本操作。包括了:notify、notifyAll、wait、join、sleep、interrupt等。通过这些操作,读者已经能够操作线程在多种不同状态下进行工作切换了。当然这些方法并不是JAVA中线程的全部操作,例如还有destroy、stop、resume等操作,但这些操作都已经被淘汰(因为它们不安全)。

以上是关于Java多线程研究03-线程的基本操作(notify,notifyAll,interrupt,join,sleep)的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程研究04-线程池的使用(ThreadPoolExecutor详解)

最近在研究多线程,浅谈JAVA中多线程的几种实现方式

java 多线程 day03 线程同步

Java多线程研究02-对象锁,synchronized关键字详解

Java多线程并发03—线程上下文,线程调度

Java多线程03——线程安全和线程同步