Java并发基础(六)——线程的基本操作

        这里主要说明常用的线程操作,是我们平常用的比较多的地方。虽然简单,却有一些值得深究的地方。

 

1,新建线程

        创建线程的方法有多种,这里就用很普通的方法,直接创建Thread类的一个实例。

// 创建线程
Thread t = new Thread();
// 调用开始方法
t.start();

        这里需要注意的是,调用Thread的start()方法后,就会启动一个线程并让线程执行run()方法,换句话说,start()方法运行了,再调不调run()方法都会运行run()方法,但调用与不调用run()方法,结果确不一样。

start()和run()方法的区别

        start()方法:调用此方法,使得该线程开始执行,如果调度OK(得到CPU时间片)就会运行run()方法;这样就是多个线程并发执行,当前线程(调用start()方法返回)不等待run()方法执行而直接执行下面的代码。run()方法称为线程体。

为什么会这样呢?

        从源码来分析,线程Thread类,也是实现了Runnable接口。而Runnable接口,只有一个run()抽象方法。根据Java面向对象思想,Thread类的实例是需要重写run()方法的,Thread类的实例也会运行run方法。从之方面来讲,单纯使用接口来定义线程Thread,避免重写Thread的run()方法,可以说是最常用的方法。

 

        还有一种情况是线程创建后不调用start()方法,直接调用run()方法,也能正常执行,但却不能创建一个线程。它会在当前线程线(Thread会在本地拥有当前线程)程中调用run()方法,算是作为一个普通的方法调用。如此代码:

// 创建线程
Thread t = new Thread();
// 直接调用运行方法
t.run();

        run()方法:Thread的子类需要重写该方法。run()方法只是类的一个普通方法,如果直接调用run()方法,程序中只有主线程一个线程,不能开启一个线程。不要用run()方法来开启新线程,它只会在当前线程中串行执行run()方法中的代码。

这些问题我以前也不清楚,之前面试的时候做过一道这样的题目,当时有点模糊于是把题目记下来了,回来了自己尝试了下。发现了与众不同的地方。

 

        下面举个例子,具体的说明start()和run()方法的区别。

    public static void main(String args[]){
        Thread t = new Thread(){
            public void run(){
                printPong();
            }
        };
        t.start();
        printPing();
    }


    public static void printPing(){
        System.out.println("PING");
    }


    public static void printPong(){
        System.out.println("PONG");
    }

        打印结果:

         启动线程后,直接运行后面的代码。

    public static void main(String args[]){
        Thread t = new Thread(){
            public void run(){
                printPong();
            }
        };
    //  t.start();
        t.run();
        printPing();
    }


    public static void printPing(){
        System.out.println("PING");
    }


    public static void printPong(){
        System.out.println("PONG");
    }

        打印结果:

        因为先调用run()方法,这里的run()方法只是一个普通方法,所以这样就好理解了。

 

2,终止线程(stop)

        一般来讲,当线程执行完成后就会结束,不需要手动关闭。但有些例外情况,需要手动去关闭它。比如一些做外服务端的后台线程,可能会常驻系统。它们的执行体有可能本身是一个大的无穷循环体,来提供某些服务。

         stop()方法:如果要一个线程中止,只需要调用Thread的stop()方法即可,很是方便。但若用Eclipse之类的IDE开发时,会发现stop()方法是一个废弃的方法。一般来说,如果在是废弃的方法,就是不推荐使用它。将来也可能会被JDK删除掉。

        为什么将stop()方法废弃呢?因为它过于简单粗暴。它会强行把正在执行的线程停止,如果这个线程中有两步是修改数据库的数据,如果只执行了第一步就被停止了,那可能会造成数据不一致的问题。因为程序中一个操作中的两个数据库修改一般会是关联的。

        stop()方法在结束运行的线程时,会直接终止线程。并立即释放掉这个线程所持有的锁,而这把锁正是维持对象一致性的。

 

3,线程中断(interrupt)

         线程中断是一种重要的线程协作机制。它比stop()方法,以一套自有的机制完善线程退出的功能。严格来讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告诉目标线程有人想要你退出。目标线程在接收到通知后要怎么处理,就完全由目标线程自己决定。如果线程中断后,线程立即无条件退出,就又会遇到stop()方法的老问题。

         Thread类有三个方法与线程中断有关,如下:

  • interrupt()方法,会通知目标线程中断,就是设置中断的标志位。中断标志位表示当前线程已经被中断了。
  • isInterrupted()方法,会判断当前线程是否被中断(检查中断标志位)。
  • interrupted()也可以用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。

 

        下面的代码,虽然t1做了中断操作,但t1并不中断。因为并没有中断处理的逻辑,只是被置为中断状态。

    public static void main(String args[]) throws InterruptedException{
        Thread t1 = new Thread(){
            @Override
            public void run(){
                while(true){
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

         经过下面的修改,t1中断后会退出,因为加了相应的中断处理代码:如果当前线程中断了,则退出循环体,结束线程。

    public static void main(String args[]) throws InterruptedException{
        Thread t1 = new Thread(){
            @Override
            public void run(){
                while(true){
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("Current Thread Interrupted!");
                        break;
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

 

        sleep(long millis)方法:让当前线程休眠millis毫秒的时间,会抛出InterruptedException中断异常。InterruptedException异常不是运行时异常,所以程序必须捕获并且处理它,当前线程休眠时,如果被中断,这个异常就会产生。

    public static void main(String args[]) throws InterruptedException{
        Thread t1 = new Thread(){
            @Override
            public void run(){
                while(true){
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("Current Thread Interrupted!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted When Sleep");
                        // 设置中断状态
                        Thread.currentThread().interrupt();
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

         上面的代码,可以看出在catch代码块中,捕获了中断,在这里可以立即退出线程。但并没有样做,因为还必须进行后续的处理来保证数据的一致性和完整性。因此执行了interrupt()方法将当前线程置上中断标记。这样再次循环时,检查中断状态的话还是中断的。

        注意:sleep()方法由于中断而抛出异常,这时它会清除中断标记,如果不加处理,在下一次循环开始时就无法捕获这个中断,所以在异常处理中,再次设置中断标记位。

 

4,等待(wait)和通知(notify)

         为了支持多线程间的协作,JDK提供了两个非常重要的接口线程:等待wait()方法和通知nofity()方法。这两个方法不在Thread类中,而是在Object类中。这样任何类都可以调用这两个方法。

public final void wait() throws InterruptedException {wait(0);}
public final native void notify();

        当一个对象实例上调用了obj.wait()方法后,当前线程就会在这个对象上等待。会一直等到其它线程调用了obj.notify()方法为止。这时,object对象俨然成了多个线程之间的有效通信手段。

        如果一个线程调用了obj.wait()方法,它就会进入obj对象的等待队列。在这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当obj.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。这里需要注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。

        除了notify()方法外,Object对象还有一个类似的notifyAll()方法,它和notify()方法的功能基本一致,不同的是它会唤醒这个等待队列中所有等待的线程,而不是随机选择一个。

        还需要强调一点,obj.wait()方法不能随便使用。它必须包含在对应的synchronized语句中,无论是wait()方法或notify()方法都需要首先获得目标对象的一个监视器。现有T1和T2两个线程,T1在正确执行wait()方法前,必须获得obj对象的监视器。而wait()方法在执行后,会释放这个监视器。这样做的目的是使其它等待在obj对象上的线程不至于因为T1的休眠而全部无法正常执行。

       T2在调用notify()方法之前,也必须获得obj对象的监视器。所幸此时T1已经释放了这个监视器(执行notify()方法,肯定是已经执行了wait()方法),T2可以顺利的获得obj对象的监视器。接着T2执行notify()方法尝试唤醒一个等待线程,这里假设唤醒了线程T1。T1被唤醒后,做的第一件事并不是执行后续的代码,而是尝试重新获得obj对象的监视器,而这个监视器也正是T1在执行wait()方法前所持有的那个。如果暂时无法获得,则T1还必须要等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。

        上述代码,开启了T1和T2两个线程。T1线程在第14行执行了object.wait()方法。在执行wait()方法之前,T1先申请了object的对象锁。因此在执行object.wait()时,它是持有object的对象锁的。wait()方法执行后,T1会进行等待,并释放object的对象锁。T2在执行notify()方法之前会先获得object的对象锁。为了效果明显,特意在notify()方法执行后让T2休眠了2秒。这样可更明显的说明,T1在得到notify()方法通知后,还是会先尝试获得object的对象锁。打印结果如下:

        在T2通知T1继续执行时(即打印“T2 end!”后),T1并不能立即继续执行,而是要等待T2释放object对象锁,并重新成功获得锁后才能继续执行(因为休眠了2秒)。

        注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

 

5,挂起(suspend)和继续执行(resume)

        线程挂起(suspend)和继续执行(resume)这两个操作是一对相反的操作。被挂起的线程,必须要等到resmue()方法操作后,才能继续执行。但它已被标注为废弃方法,就是不推荐使用了。

        不推荐使用suspend()方法去挂起线程是因为suspend()方法在导致线程暂停的同时,并不会释放任何资源。这样其它任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程执行了resume()方法,被挂起的线程才能继续。从而其它所有阻塞在相关锁上的线程也可以继续执行。但是如果resume()方法操作意外地在suspend()方法前就执行了的话,那被挂起的线程可能很难有机会被继续执行。并且,更为严重的是它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable,这也会严重影响对系统当前状态的判断。

        上述代码,开启了t1和t2两个线程。它们会在第18行通过对象锁u实现对临界区的访问。两个线程启动后,在主函数第30和31两行对其进行resume()方法操作,目标是让它们得以继续运行,接着主函数等待两个线程的结束。执行结果如下:

        表明两个线程先后进入临界区,但是程序并不会退出,而是会挂起。使用jstack命令打印系统的线程信息可以看到:

        此时需要注意,在当前系统中,线程t2其实是被挂起的。但它的线程状态确实是RUNNABLE,这很可能造成误判当前系统的状态。同时,虽然主函数中已经调用了resume()方法,但由于时间先后顺序,那个resume并没有生效!这就导致了线程t2被永远挂起,并且永远占用了对象u的锁。这对系统来讲是致命的。

        如果需要一个比较可靠的suspend()方法,该怎么办呢?上面提到的wait()方法和notify()方法,这也不是一件难事。下面的代码是通过wait()方法和notify()方法,在应用层实现suspend()方法和resume()方法功能的例子。


public class GoodSuspend {

    public static Object u = new Object();


    public static class ChangeObjectThread extends Thread {
        // 标记变量,表示当前线程是否被挂起
        volatile boolean suspendme = false;

        // 挂起线程
        public void suspendMe(){
            suspendme = true;
        }

        // 继续执行线程
        public void resumeMe(){
            suspendme = false;
            synchronized (this){
                notify();
            }
        }

        @Override
        public void run(){
            while (true){
                synchronized (this){
                    while (suspendme){
                        try {
                            wait();
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
                synchronized (u){
                    System.out.println("in ChangeObjectThread");
                }
                Thread.yield();
            }
        }
    }


    public static class ReadObjectThread extends Thread{
        @Override
        public void run(){
            while (true){
                synchronized (u){
                    System.out.println("in ReadObjectThread");
                }
                Thread.yield();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        ChangeObjectThread t1 = new ChangeObjectThread();
        ReadObjectThread t2 = new ReadObjectThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        
        t1.suspendMe();
        System.out.println("suspend t1 2 sec");
        Thread.sleep(2000);

        System.out.println("resume t1");
        t1.resumeMe();
    }

}

        上述代码中,有一个代码段(如下)会先检查当前线程是否被挂起,如果是则执行wait()方法进行等待。否则进行正常处理。当线程继续执行时,resumeMe()方法被调用,线程t1得到一个继续执行的notify()方法通知,并且清除了挂起标记,从而得以正常执行。

synchronized (this){
                      while (suspendme){
                          try {
                              wait();
                          }catch (InterruptedException e){
                              e.printStackTrace();
                          }
                      }
                   }

 

 

6,等待线程结束(join)和谦让(yield)

        在很多情况下,线程之间的协作和人与人之间的协作非常类似。一种非常常见的合作方式就是分工合作。在多线程应用中,很多时候,一个线程的输入可能非常依赖另一个或多个线程的输出,此时这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能。如下所示,显示了三个join()方法:

  • 第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。
  • 第二个方法给出了一个最大等待时间,如果超出设定时间,目标线程还在执行的话,当前线程也会因为等不及了而继续往下执行。

        英文join翻译成中文就是加入的意思。一个线程要加入另外一个线程,最好的方法就是等着它一起走。

        这里提供一个简单的join()方法实现以供参考:

public class JoinMain {

    public volatile static int i = 0;

    public static class AddThread extends Thread{
        @Override
        public void run(){
            for(i = 0; i < 10000000; i ++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AddThread at = new AddThread();
        at.start();
        at.join();
        System.out.println(i);
    }

}

        在主函数main()中,如果不使用join()方法等待AddThread,那得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值已经输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,所以在join()方法返回时,AddThread已经执行完成,因此i总是10000000.

        有关join()方法,还要补充一点,join()方法的本质是调用线程wait()方法在当前线程对象实例上。下面是JDK中join()方法实现的核心代码片段:

 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);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

        可以看到,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出当前调用notifyAll()方法通知所有的等待线程继续执行。因此值得注意的一点是:不要在应用程序中,在Thread对象实例上使用类似 wait()方法或notify()方法等,因为这很可能影响系统API的工作,或被系统API所影响

另外一个比较有趣的方法是Thread的yield(),它的定义如下:

public static native void yield();

        这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意的是,让出CPU并不是表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配就不一定了。因些对yield()方法的调用就好像是说:“我已经完成了一些最重要的工作了,我可以休息下了,可以给其他线程一些工作机会啦!”。

        如果觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那可以在适当的时候启用Thread.yield()方法,给予其它重要线程更多的工作机会。

 

注:以上内容参考《实战Java高并发程序设计(第2版)》。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值