`
liulanghan110
  • 浏览: 1063942 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

什么是线程,如何创建线程

    博客分类:
  • JAVA
 
阅读更多

如果你学习过操作系统,那么一定对进程的概念非常熟悉,其实,几乎每种操作系统都支持进程——进程就是在某种程度上相互隔离的、独立运行的程序。进程的引入大大提高了任务并发执行的效率,但是,进程也因为耗费资源太大等缺陷限制了它在并行处理方面的发展。不过线程的引入改变了这一状况,线程也称做轻量级进程。就像进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与独立的进程相比,进程中的线程之间的独立程度要小。它们共享内存、文件句柄和其他每个进程应有的状态。

线程的出现也并不是为了取代进程,而是对进程的功能作了扩展。进程可以支持多个线程,它们看似同时执行,但相互之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线程之间共享信息变得更容易,但你必须小心,确保它们不会妨碍同一进程里的其他线程。

目前,大多数的操作系统都支持线程,包括 Linux、Solaris 和 Windows NT/2003,都可以利用多个处理器调度线程在任何可用的处理器上执行。如果某个程序有多个活动线程,那么还可以同时调度多个线程。在精心设计的程序中,使用多个线程可以提高程序吞吐量和性能。在某些情况下,使用线程还可以使程序编写和维护起来更简单。虽然线程可以大大简化许多类型的应用程序,但是过度使用线程可能会危及程序的性能及其可维护性。不要忘记,线程同样也在消耗资源。因此,在不降低性能的情况下,创建一定数量的线程才是真正有用的。

Java 成为第一个在语言本身中显式地包含线程的主流编程语言,它使针对线程的操作不再那么神秘,因为它已经不再把线程看做是底层操作系统的工具。不过,虽然Java 提供的线程工具和 API 看似简单,但是编写有效使用线程的复杂程序并不十分容易。因为有多个线程共存在相同的内存空间中并共享相同的变量,所以必须小心使用,从而确保线程不会互相干扰。本章就将介绍关于线程的各种用法。

4.1 什么是线程,如何创建线程

问题

在网络中,数据传输的速率是远远低于计算机的处理能力的,就本地文件的读写而言,其读写速度也远低于CPU的处理能力。在传统的单任务环境中,程序必须等待上一个任务完成以后才能执行下一个任务。例如当前某个程序运行过程中需要等待用户键盘输入的数据,由于键盘输入的速度相对于CPU的执行速度而言要慢得多,这时候CPU就会空闲下来,直到收到键盘输入的数据,程序继续执行。

解决这种问题的办法就是引入多线程技术,Java提供了这样的技术,利用多线程技术编写的程序,可以使计算机同时并行运行多个相对独立的任务。例如,可以创建一个线程来负责数据的输入和输出,而创建另一个线程在后台进行其他的数据处理,如果输入输出线程在接收数据时阻塞,而处理数据的线程仍然可以运行。这样,多线程程序设计就大大提高了程序执行效率和处理能力。那么什么是线程,如何创建线程呢?

解决思路

在掌握如何创建线程之前,先要了解一下什么是进程。进程(process)本质上是一个执行的程序。操作系统引入进程以后就允许计算机可以同时运行两个或两个以上的程序,这就是多任务的处理模式。每一个进程都有自己独立的一块内存空间、一组系统资源。在进程概念中,每一个进程的内部数据和状态都是完全独立的。例如,基于进程的多任务处理功能不仅可以使我们在操作系统中使用记事本编辑文档,而且还可以同时听歌和看电影。

线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序的流控制,但与进程不同的是,同类的多个线程是共享同一块内存空间和一组系统资源的,而线程本身的数据通常只有微处理器的寄存器数据,以及一个供程序执行时使用的堆栈。所以系统在产生一个线程,或者在各个线程之间切换时,负担要比进程小得多,正因如此,线程也被称为轻型进程(light-weight process)。一个进程中可以包含多个线程。

多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句可以看上去几乎在同一时间内同时运行。

同时运行的含义是指操作系统中管理的时间片会平均地分给每个线程,从而保证所有的线程都能够在极短的时间内得到处理。每一时间片内只能执行一个线程,但由于时间片是一个很小的时间单元,每一个线程又是很小的代码段,因此,操作系统能够在很短的时间内进行线程的切换,所以看起来就好像是多个任务可以同时执行。

Java提供了线程类Thread来创建多线程的程序。其实,创建线程与创建普通的类的对象的操作是一样的,而线程就是Thread类或其子类的实例对象。每个Thread对象描述了一个单独的线程。要产生一个线程,有两种方法:

◆需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法;
◆实现Runnalbe接口,重载Runnalbe接口中的run()方法。

具体步骤

1、扩展Thread类来创建线程

首先,需要通过创建一个新类来扩展Thread类,这个新类就成为Thread类的子类。接着在该子类中重写Thread类的run()方法,此时方法体内的程序就是将来要在新建线程中执行的代码。

示例如下所示:

class SubThread extends Thread
{
…
public void run() 
{
// 新建线程所要完成的工作
}
…
}

 

接着要创建该子类的对象,此时一个新的线程就被创建了,创建新线程时会用到Thread 类定义的如下两个构造函数:

◆public Thread()
◆public Thread(String name)

其中,第一个构造函数是Thread类默认的构造函数,不指定参数;第二个构造函数可以为新建的线程指定一个名称,该名称就是字符串参数name的值。

 

建立了新的线程对象以后,它并不运行,而是直到调用了该对象的start()方法,该方法在Thread 类中定义,在Thread 类的子类中被覆盖。它的作用是启动一个新的线程,并在该线程上运行子类对象中的run()方法。

start()方法声明格式为:

public void start()

 示例如下所示:

SubThread s = new SubThread();   // 创建Thread子类的对象
s.start(); // 启动一个新的线程

 当一个类继承Thread类时,它必须重写run()方法,这个方法是新线程的入口。如     果Thread类的这个子类没有覆盖run()方法,那么程序会调用Thread类的run()方法,只    不过该run()方法什么也不做,此时新线程一创建就结束了。这种线程对程序来说是没有  任何意义的,所以在这里提醒读者在创建线程的时候一定不要忘了覆盖Thread类的run()方法。

下面举一个完整的例子来演示通过扩展Thread类创建线程的过程。程序代码如下所示:

// 例4.1.1  ThreadDemo.java
class NewThread extends Thread  // 通过继承Thread类来创建一个新的线程
{ 
NewThread()// 构造方法
{     
super("Thread Demo");    // 定义线程的名字
System.out.println("New thread: " + getName());
}
public void run()  // 覆写run()方法,这是线程的入口
{   
while( true )
{
System.out.println(Thread.currentThread().getName()+
"  is running");
}
}
}
class ThreadDemo 
{
public static void main(String args[]) 
{
NewThread thd = new NewThread(); // 创建一个新线程
thd.start();          //启动线程,调用NewThread类对象的run()方法
}
}

 可以看到,新的线程是由实例化NewThread类的对象创建的,该NewThread类可以通过继承java.lang.Thread类来得到。其中,在NewThread类中,构造函数里调用了super()方法。该方法将调用父类Thread下列形式的构造函数:

public Thread(String threadName)

 这里,threadName指定线程名称(当然也可以不指定线程的名称,而由系统自动为新建线程提供名称)。在NewThread类中通过覆写run()方法来规定线程所要实现的内容。此外,需要注意的是,启动新线程执行时必须调用start()方法。程序结果如图4.1.1所示:

在上面的代码中,使用了Thread类的currentThread()静态方法来获得当前程序执行时所对应的那个线程对象,又通过线程对象的getName()方法,得到了当前线程的名字。这些方法都可以在JDK帮助文档中查到。因此,善于利用JDK帮助文档来获取有关类的更多信息,可以方便程序的编写。

在上面代码的run()方法中,由于循环条件始终为true,因此,屏幕上会不断地输出Thread Demo is running,新建的线程永远不会结束,这当然不是所希望的结果。这里所希望的是可以合理的设置循环条件来有效地控制线程的终止。所以,在run()方法中使用到循环控制的时候一定要小心使用,否则局面难以控制。

其实,针对前面的程序做一些改动。可以让这个程序实现一个非常有用的功能。

 

// 例4.1.2  ThreadDemo2.java
class NewThread extends Thread // 通过继承Thread类来创建一个新的线程
{  
NewThread(String name)   // 构造方法
{
super(name);
}
public void run()  // 重写run()方法,这是线程的入口
{
for( int i=10 ; i>0 ;i--)  // 循环执行10次
{
try
{
System.out.println("left time: "+ i);
Thread.sleep(1000);   //当前线程睡眠1000毫秒
}catch(InterruptedException e){     //处理异常
System.out.println(e.getMessage());
}
}
System.out.println("game is over,bye!");
}
}
class ThreadDemo2
{
public static void main(String args[]) 
{
NewThread thd = new NewThread("Thread Demo"); // 创建一个新的线程
thd.start();          //启动线程,调用NewThread类对象的run()方法
}
}

 程序输出结果如图4.1.2 所示:

 

通过这个程序看到了什么?在run()方法体中,实现了一个倒计时的功能。线程通过循环控制,每隔一秒输出一次剩余的时间,循环结束时输出"game is over,bye!",线程也随之结束。可见这个程序中新建的线程不是死循环,而是通过一些条件来对线程的起始进行了控制,从而实现了倒计时的功能。

在这个程序中,还可以看到,在给线程起名字的时候可以通过创建线程的时候来实现。因为查阅JDK的帮助文档,会发现Thread类除了默认的构造函数之外,还有很多带参数的构造函数,只不过在这里是用到了public Thread(String name)这个构造方法。

并不是在创建线程对象的时候给线程起个名字就可以了,还应该在线程类的子类中定义相应的构造函数才行,这个构造函数的形式如下:

 

SubThread(String name)   // 线程子类的构造方法
{
super(name); 
}

 如果不这样做,程序编译会提示错误,读者可以想想为什么。也可以将上面程序中NewThread类的构造方法注释掉,编译一下程序,看到错误提示后,再去思考这个问题。

在这个程序中还用到了try…catch语句,它用来捕获程序中可能发生的异常,而产生异常的原因是程序中使用了Thread.sleep()这样的方法。通过查阅JDK的帮助文档,可以看到线程类的sleep()方法的完整格式如下:

 

public static void sleep(long millis) throws InterruptedException

 看到这个throws关键字,想必读者就应知道为什么使用try…catch语句了。由于这个方法可能会抛出一个中断异常,因此,有必要在程序调用这个方法时对可能发生的异常进行处理。此外,这个方法是静态的,所以可以通过类名直接调用。

2、实现Runnable接口来创建线程

除了扩展Thread类可以创建线程之外,还可以通过定义一个实现了Runnable接口的类来创建线程。为了将来程序执行时可以进入线程,在这个类中必须实现Runnable接口中唯一提供的run()方法。

示例如下所示:

class OneThread implements Runnable
{
…
public void run() 
{
// 新建线程所要完成的工作
}
}

 当定义好一个实现了Runnable接口的类以后,还不能直接去创建线程对象,要想真正去创建一个线程,还必须在类的内部实例化一个Thread类的对象。此时,会用到Thread 类定义的如下两个构造函数:

public Thread(Runnable target)
public Thread(Runnable target,String name)

 在这两个构造函数中,参数target定义了一个实现了Runnable接口的类的对象引用。新建的线程将来就是要执行这个对象中的run()方法。而新建线程的名字可以通过第二个构造方法中的参数name来指定。

示例如下所示:

OneThread onethread = new OneThread();
Thread newthread = new Thread(onethread);

 此时,新线程对象才被创建,如果想要执行该线程的run()方法,则仍然需要通过调用start()方法来实现。例如:

newthread.start();

 要想创建新的线程对象,这两条语句缺一不可。此后程序会在堆内存中实实在在地创建一个OneThread类的实例对象,该对象中包含了一个线程对象newthread。newthread对象会通过调用start()方法来执行它自己的run()方法。随着run()方法的结束,线程对象newthread的生命也将结束,但是onethread对象还会存在于堆内存当中。如果希望在实际编程当中一旦线程结束,即释放与线程有关的所有资源,可以使用创建匿名对象的方法来创建这个线程,格式如下所示:

new Thread(new OneThread).start();

 这样一来,该线程一旦运行结束,所有与该线程有关的资源都将成为垃圾,这样就可以在特定的时间内被Java的垃圾回收机制予以回收,释放所占用内存,提高程序的效率。

下面这个程序是通过实现Runnable接口来创建的线程,可以将它和前面的例4.1.2的程序进行比较。

// 例4.1.3  ThreadDemo3.java
class NewThread implements Runnable // 实现了Runnable接口
{  
public void run() // 覆写Runnable 接口中唯一的run()方法,这是线程的入口
{
for( int i=10 ; i>0 ;i--)
{
try
{
System.out.println("left time: "+ i);
Thread.sleep(1000);   //当前线程睡眠1000毫秒
}catch(InterruptedException e){     //处理异常
System.out.println(e.getMessage());
}
}
System.out.println("game is over,bye!");
}
}
class ThreadDemo3
{
public static void main(String args[]) 
{
NewThread newthread = new NewThread(); 
Thread thd = new Thread(newthread, "Thread Demo")
thd.start();          //启动线程,调用run()方法
}
}

 编译并运行这个程序,可以看到程序执行的结果和例4.1.2的程序输出的结果是完全一样的,因此,读者可以在创建线程的时候选择任意一种方式来实现。

专家说明

为了使程序达到优异的性能,可以利用创建线程来完成那些任务。因为一个线程就是一个独立的执行通道,在没有特殊的要求之下,多个线程之间彼此独立运行,互不干扰。而且带线程的程序通常比没有带线程的程序运行得要快,因此线程常用在网络和图形用户界面等程序设计当中,这一优势在多处理器的计算机上更加明显。本节中不仅要理解什么是线程,而且还应掌握两种创建线程的方法,为以后在程序中使用多线程技术打下坚实的基础。

专家指点

学习了本节以后,有很多读者都会问到这样的问题:为什么Java要提供两种方法来创建线程呢?它们都有哪些区别?相比而言,哪一种方法更好呢?

在Java中,类仅支持单继承,也就是说,当定义一个新的类的时候,它只能扩展一个外部类.这样,如果创建自定义线程类的时候是通过扩展Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。

还有一点最重要的就是使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享,这一点会在第4.2节中介绍。

相关问题

如果程序当中没有显式地创建线程,那么是不是程序中就没有线程存在呢?其实,当Java程序启动时,有一个线程立刻就会运行,该线程是自动创建的,它就是程序的主线程(main thread),它在程序开始时就执行了。

主线程的产生可以完成两方面的任务:

◆它是产生其他子线程的线程
◆通常它必须最后完成执行,这样它可以执行各种关闭操作。

下面举一个能够显示主线程运行状态的例子,程序代码如下。

 

// 例4.1.4  ThreadDemo4.java
class NewThread extends Thread
{
NewThread(String name)
{
super(name);
}
public void run()    
{ 
for (int count = 10,row = 0; row < 10; row++)  //输出10行*
{   
for (int i = 0; i < count; i++)
System.out.print('*');
System.out.println();
}
System.out.println(currentThread().getName()+"  is over");
}
}
class ThreadDemo4
{
public static void main(String argv[])
{
// 下面两行代码用来新建一个线程
NewThread thd = new NewThread("new Thread");  
thd.start();        
// 下面的代码用来控制主线程
for(int i=0;i<10;i++)
{
System.out.println(Thread.currentThread().getName()+"  is 
running");
}
System.out.println(Thread.currentThread().getName()+" is over");
}
}

 尽管主线程在程序启动时自动创建,但它可以被一个Thread对象控制。要想这样做,必须在主类中调用Thread类的方法currentThread()来获得主线程的一个引用。

再来研究一下这个Thread类的currentThread()方法到底都做了什么,通过查阅JDK帮助文档,发现该方法是Thread类的公有的静态成员。它的语法形式如下:

 

static Thread currentThread( )

 该方法如果在主类中被使用,那么它将返回主线程的引用,如果在一个Thread类的派生子类中被使用,则它将返回由该派生子类所产生的新线程的引用。因此,一旦获得主线程的引用,就可以像控制其他线程那样控制主线程了。

编译程序并运行,得到这样的结果:

 

从结果来看,并不是先输出10行“**********”符号,然后再输出10行"main is running",而是交错输出。这就是因为采用了线程的方式,由于操作系统在调度线程的时候并不是按顺序进行的,而是将时间片轮流分给每一个线程,具体每一次分给哪个线程也是不确定的,但可以保证在很短的时间内,每一个线程都有被执行的机会,因此程序显示出了上面的结果。当然,如果再次运行程序,还会得到不同的输出结果。当循环的次数越多,输出的结果越能说明这个问题。

随机性地分配时间片给一个线程使用并不是我们所期望的结果,引入线程是为了能够充分利用系统资源,但如果程序中存在很多线程,那么程序的输出结果会因为这种随机性而变得面目全非。为了更好地控制多个线程之间能够合理地使用时间片,可以通过一些方法来合理地调度线程,从而既能够正确地输出期望的结果,又充分利用系统资源。想了解有关内容,读者可以参考第4.2节。

前面提到程序的生命周期时是这样介绍的:程序从main()方法这个入口进入,在main()方法中可以调用其他方法和成员或者其他类及类的方法和成员,main()方法执行完毕,整个程序也就结束了。但是在这个程序中可以看到这样的情况,即new Thread这个线程结束的时间是在主线程结束之后。这就说明Java允许其他线程在主线程之后结束。也证明这样一个事实,那就是,只有当所有线程都执行结束时,整个程序才真正结束。可见,前面的说法并无错误,只不过这里引入了线程的概念。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics