Java多线程初学者指南系列教程_java多线程服务器教材-程序员宅基地

技术标签: java  string  多线程  thread  join  任务  

http://developer.51cto.com/art/200911/162925.htm

  • 本系列文章从Java线程的基本概念讲起,介绍了线程的创建,线程的生命周期,线程方法的使用,线程的数据传递以及线程的同步等内容。

本系列来自NokiaGuy的“真的有外星人吗 ”博客,系列名称为《Java多线程初学者指南》。整个系列介绍了Java线程的好处,概念和原理,基础操作,高阶操作等内容。

51CTO编辑推荐: Java线程从入门到实践

系列目录:

  1. 线程简介
    介绍了什么是Java的线程及多线程的好处。由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。
  2. 用Thread类创建线程
    在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。任何一个Java程序都必须有一个主线程。学习Java多线程,需要先从用Thread类创建线程开始。
  3. 使用Runnable接口创建线程
    讲解如何使用Runnable接口创建线程。实现Runnable接口的类必须使用Thread类的实例才能创建线程。
  4. 线程的生命周期
    与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。
  5. join方法的使用
    join方法的功能就是使异步执行的线程变成同步执行。
  6. 慎重使用volatile关键字
    volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。使用它有一定的限制。
  7. 向线程传递数据的三种方法
    由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。
  8. 从线程返回数据的两种方法
    从线程中返回数据和向线程传递数据类似。也可以通过类成员以及回调函数来返回数据。
  9. 使用Synchronized关键字同步类方法
    要达成Java多线程的run方法同步,需要在void和public之间加上synchronized关键字。
  10. 使用Synchronized块同步方法
    我们不仅可以通过synchronized块来同步一个对象变量,也可以使用synchronized块来同步类中的静态方法和非静态方法。
  11. 使用Synchronized块同步变量
    讲解如何使用Synchronized块同步变量。我们可以通过synchronized块来同步特定的静态或非静态方法。

希望通过这个系列的学习,能够帮助读者掌握Java多线程的概念和一些基本操作。在Java线程的实际应用方面还有很多需要考虑的事项,还需要读者们在实践中逐渐摸索。

  • 初学Java多线程:线程简介

  • 本文为Java多线程初学者系列的第一篇,简单介绍了什么是Java的线程及多线程的好处。由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。

一、线程概述

线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进 程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程。

进程和线程是现代操作系统中两个必不可少的运行模型。在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程 (由用户程序建立的进程); 一个进程中可以有一个或多个线程。进程和进程之间不共享内存,也就是说系统中的进程是在各自独立的内存空间中运行的。而一个进 程中的线可以共享系统分派给这个进程的内存空间。

线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈, 是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。

注意:任何一个线程在建立时都会执行一个函数,这个函数叫做线程执行函数。也可以将这个函数看做线程的入口点(类似于程序中的main函数)。无论 使用什么语言或技术来建立线程,都必须执行这个函数 (这个函数的表现形式可能不一样,但都会有一个这样的函数)。如在Windows中用于建立线程的 API函数CreateThread的第三个参数就是这个执行函数的指针。

在操作系统将进程分成多个线程后,这些线程可以在操作系统的管理下并发执行,从而大大提高了程序的运行效率。虽然线程的执行从宏观上看是多个线程同 时执行,但实际上这只是操作系统的障眼法。由于一块CPU同时只能执行一条指令,因此,在拥有一块CPU的计算机上不可能同时执行两个任务。而操作系统为 了能提高程序的运行效率,在一个线程空闲时会撤下这个线程,并且会让其他的线程来执行,这种方式叫做线程调度。我们之所以从表面上看是多个线程同时执行, 是因为不同线程之间切换的时间非常短,而且在一般情况下切换非常频繁。 假设我们有线程A和B。在运行时,可能是A执行了1毫秒后,切换到B后,B又执行了 1毫秒,然后又切换到了A,A又执行1毫秒。由于1毫秒的时间对于普通人来说是很难感知的,因此,从表面看上去就象A和B同时执行一样,但实际上A和B是 交替执行的。

二、线程给我们带来的好处

如果能合理地使用线程,将会减少开发和维护成本,甚至可以改善复杂应用程序的性能。如在GUI应用程序中,还以通过线程的异步特性来更好地处理事 件;在应用服务器程序中可以通过建立多个线程来处理客户端的请求。线程甚至还可以简化虚拟机的实现,如Java虚拟机(JVM)的垃圾回收器 (garbage collector)通常运行在一个或多个线程中。因此,使用线程将会从以下五个方面来改善我们的应用程序:

1. 充分利用CPU资源

现在世界上大多数计算机只有一块CPU。因此,充分利用CPU资源显得尤为重要。当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状 态。这将造成大量的计算资源的浪费。而在程序中使用多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。这样CPU 就很难有空闲的时候。因此,CPU资源就得到了充分地利用。

2.   简化编程模型

如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。但要完成多项任务,如果还使用单线程的话,那就得在 在程序中判断每项任务是否应该执行以及什么时候执行。如显示一个时钟的时、分、秒三个指针。使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。 如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。 这样有助于开发人员对程序的理解和维护。

3.   简化异步事件的处理

当一个服务器应用程序在接收不同的客户端连接时最简单地处理方法就是为每一个客户端连接建立一个线程。然后监听线程仍然负责监听来自客户端的请求。 如果这种应用程序采用单线程来处理,当监听线程接收到一个客户端请求后,开始读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,why? 也就是说, 这个线程将无法再监听客户端请求了。而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O。但使用异步I/O方式比使用 同步I/O更难以控制,也更容易出错。因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。

4.   使GUI更有效率

使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。

在现代的GUI框架(如SWING、AWT和SWT)中都使用了一个单独的事件分派线程(event dispatch thread,EDT)来对GUI事件进行扫描。当我们按下一个按钮时,按钮的单击事件函数会在这个事件分派线程中被调用。由于EDT的任务只是对GUI 事件进行扫描,因此,这种方式对事件的反映是非常快的。

5.   节约成本

提高程序的执行效率一般有三种方法:

(1)增加计算机的CPU个数。

(2)为一个程序启动多个进程

(3)在程序中使用多 线程。

第一种方法是最容易做到的,但同时也是最昂贵的。这种方法不需要修改程序,从理论上说,任何程序都可以使用这种方法来提高执行效率。第二种方法虽然 不用购买新的硬件,但这种方式不容易共享数据,如果这个程序要完成的任务需要必须要共享数据的话,这种方式就不太方便,而且启动多个线程会消耗大量的系统 资源。第三种方法恰好弥补了第一种方法的缺点,而又继承了它们的优点。也就是说,既不需要购买CPU,也不会因为启太多的线程而占用大量的系统资源(在默 认情况下,一个线程所占的内存空间要远比一个进程所占的内存空间小得多),并且多线程可以模拟多块CPU的运行方式,因此,使用多线程是提高程序执行效率 的最廉价的方式。

三、Java的线程模型

由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。Java通过Thread类 将线程所必须的功能都封装了起来。要想建立一 个线程,必须要有一个线程执行函数,这个线程执行函数对应Thread类的run方法。Thread类还有一个start方法,这个方法负责建立线程,相 当于调用Windows的建立线程函数CreateThread。 当调用start方法后,如果线程建立成功,并自动调用Thread类的run方法。 因 此,任何继承Thread的Java类都可以通过Thread类的start方法来建立线程。如果想运行自己的线程执行函数,那就要覆盖Thread类的 run方法。

在J ava的线程模型中除了Thread类,还有一个标识某个Java类是否可作为线程类的接口Runnable,这个接口只有一个抽象方法 run,也就是Java线程模型的线程执行函数。 ,一个线程类的唯一标准就是这个类是否实现了Runnable接口的run方法,也就是说,拥有线程 执行函数的类就是线程类。

上面可以看出,在Java中建立线程有两种方法,一种是继承Thread类,另一种是实现Runnable接口,并通过Thread和实现 Runnable的类来建立线程, 其实这两种方法从本质上说是一种方法,即都是通过Thread类来建立线程,并运行run方法的。但它们的大区别是通过 继承Thread类来建立线程,虽然在实现起来更容易,但由于Java不支持多继承,因此,这个线程类如果继承了Thread,就不能再继承其他的类了, 因此,Java线程模型提供了通过实现Runnable接口的方法来建立线程,这样线程类可以在必要的时候继承和业务有关的类,而不是Thread类。

  • 初学Java多线程:用Thread类创建线程

  • 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。任何一个Java程序都必须有一个主线程。学习Java多线程,需要先从用Thread类创建线程开始。

在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。在使用Runnable接口时需要建立一个 Thread实例。因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。Thread类的构造 方法被重载了八次,构造方法如下:

 
 
  
  1. public  Thread( );  
  2. public  Thread(Runnable target);  
  3. public  Thread(String name);  
  4. public  Thread(Runnable target, String name);  
  5. public  Thread(ThreadGroup group, Runnable target);  
  6. public  Thread(ThreadGroup group, String name);  
  7. public  Thread(ThreadGroup group, Runnable target, String name);  
  8. public  Thread(ThreadGroup group, Runnable target, String name, long  stackSize); 

Runnable target

实现了Runnable接口的类的实例。要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。

String name

线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名子,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。

ThreadGroup group

当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中。关于线程组的细节将在后面的章节详细讨论。

long stackSize

线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB。在x86平台下,默认的线程栈大小是12KB。

一个普通的Java类只要从Thread类继承,就可以成为一个线程类。并可通过Thread类的start方法来执行线程代码。虽然Thread 类的子类可以直接实例化,但在子类中必须要覆盖Thread类的run方法才能真正运行线程的代码。下面的代码给出了一个使用Thread类建立线程的例 子:

 
 
  
  1. package  mythread;  
  2.    
  3.  public  class  Thread1 extends  Thread  
  4.   {  
  5.       public  void  run()  
  6.       {  
  7.           System.out.println(this .getName());  
  8.       }  
  9.       public  static  void  main(String[] args)  
  10.       {  
  11.           System.out.println(Thread.currentThread().getName());  
  12.           Thread1 thread1 = new  Thread1();  
  13.           Thread1 thread2 = new  Thread1 ();  
  14.           thread1.start();  
  15.           thread2.start();  
  16.       }  
  17.   } 

上面的代码建立了两个线程:thread1和thread2。上述代码中的005至008行是Thread1类的run方法。当在014和015行 调用start方法时,系统会自动调用run方法。在007行使用this.getName()输出了当前线程的名字,由于在建立线程时并未指定线程名, 因此,所输出的线程名是系统的默认值,也就是Thread-n的形式。在011行输出了主线程的线程名。

上面代码的运行结果如下:

main
Thread-0
Thread-1

从上面的输出结果可以看出,第一行输出的main是主线程的名子。后面的Thread-1和Thread-2分别是thread1和thread2的输出结果。

注意:任何一个Java程序都必须有一个主线程。一般这个主线程的名子为main。只有在程序中建立另外的线程,才能算是真正的多线程程序。也就是说,多线程程序必须拥有一个以上的线程。

Thread类有一个重载构造方法可以设置线程名。除了使用构造方法在建立线程时设置线程名,还可以使用Thread类的setName方法修改线 程名。 要想通过Thread类的构造方法来设置线程名,必须在Thread的子类中使用Thread类的public Thread(String name)构造方法,因此,必须在Thread的子类中也添加一个用于传入线程名的构造方法。下面的代码给出了一个设置线程名的例子:

 
 
  
  1. package  mythread;  
  2.  
  3. public  class  Thread2 extends  Thread  
  4. {  
  5.     private  String who;  
  6.  
  7.     public  void  run()  
  8.     {  
  9.         System.out.println(who + ":"  + this .getName());  
  10.     }  
  11.     public  Thread2(String who)  
  12.     {  
  13.         super ();  
  14.         this .who = who;  
  15.     }  
  16.     public  Thread2(String who, String name)  
  17.     {  
  18.         super (name);  
  19.         this .who = who;  
  20.     }  
  21.     public  static  void  main(String[] args)  
  22.     {  
  23.         Thread2 thread1 = new  Thread2 ("thread1" "MyThread1" );  
  24.         Thread2 thread2 = new  Thread2 ("thread2" );  
  25.         Thread2 thread3 = new  Thread2 ("thread3" );  
  26.         thread2.setName("MyThread2" );  
  27.         thread1.start();  
  28.         thread2.start();  
  29.         thread3.start();  
  30.     }  
  31.   

在类中有两个构造方法:

第011行:public sample2_2(String who)

这个构造方法有一个参数:who。这个参数用来标识当前建立的线程。在这个构造方法中仍然调用Thread的默认构造方法public Thread( )。

第016行:public sample2_2(String who, String name)

这个构造方法中的who和第一个构造方法的who的含义一样,而name参数就是线程的名名。在这个构造方法中调用了Thread类的public Thread(String name)构造方法,也就是第018行的super(name)。

在main方法中建立了三个线程:thread1、thread2和thread3。其中thread1通过构造方法来设置线程名,thread2通过setName方法来修改线程名,thread3未设置线程名。

运行结果如下:

thread1:MyThread1
thread2:MyThread2
thread3:Thread-1

从上面的输出结果可以看出,thread1和thread2的线程名都已经修改了,而thread3的线程名仍然为默认值:Thread-1。 thread3的线程名之所以不是Thread-2,而是Thread-1,这是因为在026行已经指定了thread2的Name,因此,启动 thread3时就将thread3的线程名设为Thread-1。因此就会得到上面的输出结果。

注意:在调用start方法前后都可以使用setName设置线程名,但在调用start方法后使用setName修改线程名,会产生不确定性,也 就是说可能在run方法执行完后才会执行setName。如果在run方法中要使用线程名,就会出现虽然调用了setName方法,但线程名却未修改的现 象。

Thread类的start方法不能多次调用,如不能调用两次thread1.start()方法。否则会抛出一个IllegalThreadStateException异常。

  • 初学Java多线程:使用Runnable接口创建线程

  • 这篇初学Java多线程系列为你讲解如何使用Runnable接口创建线程。实现Runnable接口的类必须使用Thread类的实例才能创建线程。

实现Runnable接口的类必须使用Thread类的实例才能创建线程。通过Runnable接口创建线程分为两步:

1. 将实现Runnable接口的类实例化。

2.     建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法。

最后通过Thread类的start方法建立线程。

下面的代码演示了如何使用Runnable接口来创建线程:

 
 
  
  1. package  mythread;  
  2.  
  3. public  class  MyRunnable implements  Runnable  
  4. {  
  5.     public  void  run()  
  6.     {  
  7.         System.out.println(Thread.currentThread().getName());  
  8.     }  
  9.     public  static  void  main(String[] args)  
  10.     {  
  11.         MyRunnable t1 = new  MyRunnable();  
  12.         MyRunnable t2 = new  MyRunnable();  
  13.         Thread thread1 = new  Thread(t1, "MyThread1" );  
  14.         Thread thread2 = new  Thread(t2);  
  15.         thread2.setName("MyThread2" );  
  16.         thread1.start();  
  17.         thread2.start();  
  18.     }  
  19. }  

上面代码的运行结果如下:

MyThread1
MyThread2

举例Java多线程的学习又更近一步了。

  • 初学Java多线程:线程的生命周期

  • 初学Java多线程系列的本部分介绍线程的生命周期。与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。

与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。 下面给出了Thread类中和这四种状态相关的方法。

 
 
  
  1. // 开始线程  
  2. public  void  start( );  
  3. public  void  run( );  
  4.  
  5. // 挂起和唤醒线程  
  6. public  void  resume( );     // 不建议使用  
  7. public  void  suspend( );    // 不建议使用  
  8. public  static  void  sleep(long  millis);  
  9. public  static  void  sleep(long  millis, int  nanos);  
  10.  
  11. // 终止线程  
  12. public  void  stop( );       // 不建议使用  
  13. public  void  interrupt( );  
  14.  
  15. // 得到线程状态  
  16. public  boolean  isAlive( );  
  17. public  boolean  isInterrupted( );  
  18. public  static  boolean  interrupted( );  
  19.  
  20. // join方法  
  21. public  void  join( ) throws  InterruptedException;  

一、创建并运行线程

线程在建立后并不马上执行run方法中的代码,而是处于等待状态。线程处于等待状态时,可以通过Thread类的方法来设置线程不各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。

当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行 状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。下面的代码演 示了线程的创建、运行和停止三个状态之间的切换,并输出了相应的isAlive返回值。

(线程建立后处于等待状态,调start方法后进入运行状态,调start后会自动调run方法)

 
 
  
  1. package  chapter2;  
  2.  
  3. public  class  LifeCycle extends  Thread  
  4. {  
  5.     public  void  run()  
  6.     {  
  7.         int  n = 0 ;  
  8.         while  ((++n) < 1000 );          
  9.     }  
  10.        
  11.     public  static  void  main(String[] args) throws  Exception  
  12.     {  
  13.         LifeCycle thread1 = new  LifeCycle();  
  14.         System.out.println("isAlive: "  + thread1.isAlive());  
  15.         thread1.start();  
  16.         System.out.println("isAlive: "  + thread1.isAlive());  
  17.         thread1.join();  // 等线程thread1结束后再继续执行   
  18.         System.out.println("thread1已经结束!" );  
  19.         System.out.println("isAlive: "  + thread1.isAlive());  
  20.     }  
  21. }  

要注意一下,在上面的代码中使用了join方法,这个方法的主要功能是保证线程的run方法完成后程序才继续运行,这个方法将在后面的文章中介绍

上面代码的运行结果:

isAlive: false
isAlive: true
thread1已经结束!
isAlive: false

二、挂起和唤醒线程

一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两 个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的 时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。

虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成一些不可预料的事情发生,因此,这两个方法被标识 为deprecated(抗议)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。下面的代码演示了 sleep、suspend和resume三个方法的使用。

 
 
  
  1. package  chapter2;  
  2.  
  3. public  class  MyThread extends  Thread  
  4. {  
  5.     class  SleepThread extends  Thread  
  6.     {  
  7.         public  void  run()  
  8.         {  
  9.             try  
  10.             {  
  11.                 sleep(2000 );  
  12.             }  
  13.             catch  (Exception e)  
  14.             {  
  15.             }  
  16.         }  
  17.     }  
  18.     public  void  run()  
  19.     {  
  20.         while  (true )  
  21.             System.out.println(new  java.util.Date().getTime());  
  22.     }  
  23.     public  static  void  main(String[] args) throws  Exception  
  24.     {  
  25.         MyThread thread = new  MyThread();  
  26.         SleepThread sleepThread = thread.new  SleepThread();  
  27.         sleepThread.start(); // 开始运行线程sleepThread  
  28.         sleepThread.join();  // 使线程sleepThread延迟2秒  
  29.         thread.start();  
  30.         boolean  flag = false ;  
  31.         while  (true )  
  32.         {  
  33.             sleep(5000 );  // 使主线程延迟5秒  
  34.             flag = !flag;  
  35.             if  (flag)  
  36.                 thread.suspend();   
  37.             else  
  38.                 thread.resume();  
  39.         }  
  40.     }  
  41. }  

从表面上看,使用sleep和suspend所产生的效果类似,但sleep方法并不等同于suspend。它们之间最大的一个区别是可以在一个线 程中通过suspend方法来挂起另外一个线程,如上面代码中在主线程中挂起了thread线程。而sleep只对当前正在执行的线程起作用。 在上面代码 中分别使sleepThread和主线程休眠了2秒和5秒。在使用sleep时要注意,不能在一个线程中来休眠另一个线程。如main方法中使用 thread.sleep(2000)方法是无法使thread线程休眠2秒的,而只能使主线程休眠2秒。

在使用sleep方法时有两点需要注意:

1. sleep方法有两个重载形式,其中一个重载形式不仅可以设毫秒,而且还可以设纳秒(1,000,000纳秒等于1毫秒)。但大多数操作系统平台上的Java虚拟机都无法精确到纳秒,因此,如果对sleep设置了纳秒,Java虚拟机将取最接近这个值的毫秒。

2. 在使用sleep方法时必须使用throws或try{...}catch{...}。因为run方法无法使用throws,所以只能使用 try{...}catch{...}。当在线程休眠的过程中,使用interrupt方法(这个方法将在2.3.3中讨论)中断线程时sleep会抛出 一个InterruptedException异常。sleep方法的定义如下:

 
 
  
  1. public  static  void  sleep(long  millis)  throws  InterruptedException  
  2. public  static  void  sleep(long  millis,  int  nanos)  throws  InterruptedException 

三、终止线程的三种方法

有三种方法可以使终止线程。

1.  使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

2.  使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。

3.  使用interrupt方法中断线程

1. 使用退出标志终止线程

当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的 任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){...}来处理。 但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制 while循环是否退出。下面给出了一个利用退出标志终止线程的例子。

 
 
  
  1. package  chapter2;  
  2.  
  3. public  class  ThreadFlag extends  Thread  
  4. {  
  5.     public  volatile  boolean  exit = false ;  
  6.  
  7.     public  void  run()  
  8.     {  
  9.         while  (!exit);  
  10.     }  
  11.     public  static  void  main(String[] args) throws  Exception  
  12.     {  
  13.         ThreadFlag thread = new  ThreadFlag();  
  14.         thread.start();  
  15.         sleep(5000 ); // 主线程延迟5秒  
  16.         thread.exit = true ;  // 终止线程thread  
  17.         thread.join();  
  18.         System.out.println("线程退出!" );  
  19.     }  
  20. }  

在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false。在定义exit时,使用了 一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,

2. 使用stop方法终止线程

使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:

thread.stop();
虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。

3. 使用interrupt方法终止线程

使用interrupt方法来终端线程可分为两种情况:

(1)线程处于阻塞状态,如使用了sleep方法。

(2)使用while(!isInterrupted()){...}来判断线程是否被中断。

在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。 下面的代码演示了在第一种情况下使用interrupt方法。

 
 
  
  1. package  chapter2;  
  2.  
  3. public  class  ThreadInterrupt extends  Thread  
  4. {  
  5.     public  void  run()  
  6.     {  
  7.         try  
  8.         {  
  9.             sleep(50000 );  // 延迟50秒  
  10.         }  
  11.         catch  (InterruptedException e)  
  12.         {  
  13.             System.out.println(e.getMessage());  
  14.         }  
  15.     }  
  16.     public  static  void  main(String[] args) throws  Exception  
  17.     {  
  18.         Thread thread = new  ThreadInterrupt();  
  19.         thread.start();  
  20.         System.out.println("在50秒之内按任意键中断线程!" );  
  21.         System.in.read();  
  22.         thread.interrupt();  
  23.         thread.join();  
  24.         System.out.println("线程已经退出!" );  
  25.     }  
  26. }  

上面代码的运行结果如下:


在50秒之内按任意键中断线程!

sleep interrupted
线程已经退出!


在调用interrupt方法后, sleep方法抛出异常,然后输出错误信息:sleep interrupted。

注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静 态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判 断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。

以上就是线程的生命周期。要进一步学习Java多线程,务必要对Java线程生命周期有着足够的认识。

  • 初学Java多线程:join方法的使用

  • 初学Java多线程系列的本章讲述join方法的使用。join方法的功能就是使异步执行的线程变成同步执行。

在上面的例子中多次使用到了Thread类的join方法。我想大家可能已经猜出来join方法的功能是什么了。对,join方法 的功能就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个 由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。 而使用join方法后,直到这个线程退出,程序才会往下执行。下面的代码演示了join的用法。

 
 
  
  1. package  mythread;  
  2.  
  3. public  class  JoinThread extends  Thread  
  4. {  
  5.     public  static  int  n = 0 ;  
  6.  
  7.     static  synchronized  void  inc()  
  8.     {  
  9.         n++;  
  10.     }  
  11.     public  void  run()  
  12.     {  
  13.         for  (int  i = 0 ; i < 10 ; i++)  
  14.             try  
  15.             {  
  16.                 inc();  
  17.                 sleep(3 );  // 为了使运行结果更随机,延迟3毫秒  
  18.                   
  19.             }  
  20.             catch  (Exception e)  
  21.             {  
  22.             }                                        
  23.     }  
  24.     public  static  void  main(String[] args) throws  Exception  
  25.     {  
  26.      
  27.         Thread threads[] = new  Thread[100 ];  
  28.         for  (int  i = 0 ; i < threads.length; i++)  // 建立100个线程  
  29.             threads[i] = new  JoinThread();  
  30.         for  (int  i = 0 ; i < threads.length; i++)   // 运行刚才建立的100个线程  
  31.             threads[i].start();  
  32.         if  (args.length > 0 )    
  33.             for  (int  i = 0 ; i < threads.length; i++)   // 100个线程都执行完后继续  
  34.                 threads[i].join();  
  35.         System.out.println("n="  + JoinThread.n);  
  36.     }  
  37. }  

在例程2-8中建立了100个线程,每个线程使静态变量n增加10。如果在这100个线程都执行完后输出n,这个n值应该是1000。

1.  测试1

使用如下的命令运行上面程序:

 
 
  
  1. java mythread.JoinThread 

程序的运行结果如下:

n=442

这个运行结果可能在不同的运行环境下有一些差异,但一般n不会等于1000。从上面的结果可以肯定,这100个线程并未都执行完就将n输出了。

2.  测试2

使用如下的命令运行上面的代码:

在上面的命令行中有一个参数join,其实在命令行中可以使用任何参数,只要有一个参数就可以,这里使用join,只是为了表明要使用join方法使这100个线程同步执行。

程序的运行结果如下:

n=1000

无论在什么样的运行环境下运行上面的命令,都会得到相同的结果:n=1000。这充分说明了这100个线程肯定是都执行完了,因此,n一定会等于1000。

  • 初学Java多线程:慎重使用volatile关键字

  • 学习Java多线程中会遇到使用volatile关键字的情况。volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。使用它有一定的限制。

volatile关键字相信了解Java多线程的读者都很清楚它的作用。volatile关键字用于声明简单类型变量,如int、 float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的 例子中的n就不是原子级别的:

 
 
  
  1. package  mythread;  
  2.  
  3. public   class  JoinThread  extends  Thread  
  4. {  
  5.      public   static   volatile   int  n =  0 ;  
  6.      public   void  run()  
  7.     {  
  8.          for  ( int  i =  0 ; i <  10 ; i++)  
  9.              try  
  10.         {  
  11.                 n = n +  1 ;  
  12.                 sleep( 3 );  // 为了使运行结果更随机,延迟3毫秒  
  13.  
  14.             }  
  15.              catch  (Exception e)  
  16.             {  
  17.             }  
  18.     }  
  19.  
  20.      public   static   void  main(String[] args)  throws  Exception  
  21.     {  
  22.  
  23.         Thread threads[] =  new  Thread[ 100 ];  
  24.          for  ( int  i =  0 ; i < threads.length; i++)  
  25.              // 建立100个线程  
  26.             threads[i] =  new  JoinThread();  
  27.          for  ( int  i =  0 ; i < threads.length; i++)  
  28.              // 运行刚才建立的100个线程  
  29.             threads[i].start();  
  30.          for  ( int  i =  0 ; i < threads.length; i++)  
  31.              // 100个线程都执行完后继续  
  32.             threads[i].join();  
  33.         System.out.println( "n="  + JoinThread.n);  
  34.     }  
  35. }  
  36.        

如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原 子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不 是原子操作:

n = n + 1;
n++;

如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:

 
 
  
  1. package  mythread;  
  2.  
  3. public   class  JoinThread  extends  Thread  
  4. {  
  5.      public   static   int  n =  0 ;  
  6.  
  7.      public   static   synchronized   void  inc()  
  8.     {  
  9.         n++;  
  10.     }  
  11.      public   void  run()  
  12.     {  
  13.          for  ( int  i =  0 ; i <  10 ; i++)  
  14.              try  
  15.             {  
  16.                 inc();  // n = n + 1 改成了 inc();  
  17.                 sleep( 3 );  // 为了使运行结果更随机,延迟3毫秒  
  18.  
  19.             }  
  20.              catch  (Exception e)  
  21.             {  
  22.             }  
  23.     }  
  24.  
  25.      public   static   void  main(String[] args)  throws  Exception  
  26.     {  
  27.  
  28.         Thread threads[] =  new  Thread[ 100 ];  
  29.          for  ( int  i =  0 ; i < threads.length; i++)  
  30.              // 建立100个线程  
  31.             threads[i] =  new  JoinThread();  
  32.          for  ( int  i =  0 ; i < threads.length; i++)  
  33.              // 运行刚才建立的100个线程  
  34.             threads[i].start();  
  35.          for  ( int  i =  0 ; i < threads.length; i++)  
  36.              // 100个线程都执行完后继续  
  37.             threads[i].join();  
  38.         System.out.println( "n="  + JoinThread.n);  
  39.     }  
  40. }  

上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字 时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/happy_6678/article/details/5933991

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签