黑马程序员——多线程2:应用

来源:互联网 发布:淘宝布衣柜 编辑:程序博客网 时间:2024/05/17 07:31

------- android培训、java培训、期待与您交流! ----------

1. 创建并执行线程

       在前面的内容中我们说到,主线程在执行的过程中,还会自动创建一个线程去负责垃圾回收,那么我们也可以手动创建一个进程去执行我们指定的代码。

1) 创建线程的第一种方法

       Java类库中表示线程的类称为Thread,存在于java.lang包当中。API文档中对它的描述是:该类表示程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程。API文档还告诉我们,创建新执行线程有两种方法,这里我们先说第一种方法:将类声明为Thread类的子类。该子类应重写Thread类的run方法。这一方式类似于异常类的定义——如果想要定义为异常类,就要继承Exception,或者RuntimeException。我们通过下面的代码,演示如何定义线程类。

代码1:

//定义线程类需要继承Thread类,并复写run方法class SubThread extends Thread{//复写run方法public void run(){System.out.println("SubThreadrun");}}public class ThreadDemo{public static void main(String[] args){//创建线程,并执行其中的run方法。SubThreadst= newSubThread();//调用线程对象的start方法,启动该线程st.start();}}
运行结果为:

SubThread run

 

上述代码就是对定义并创建线程的一个简单演示,其步骤可以简单总结为:

第一步:定义一个类并继承Thread类。通常为了简化代码书写,可以使用匿名内部类的方式。

第二步:复写Thread类中的run方法。

第三步:调用线程对象的start方法——启动该线程,并执行run方法。

 

在这里我们对第二步和第三步进行一些简单的解释:

        关于第二步:之所以我们要创建并执行一个线程,就是为了使其在主线程执行的同时,帮助我们执行另外的一些代码,以达到优化程序的目的。那么Thread类作为对线程这一类事物的描述,很自然其内部就会定义某个方法来存储需要被执行的代码——run方法。与此相对应的是,主线程需要执行的代码存放在main方法中。但是Java标准类库中的Thread类其run方法并没有定义任何内容,所以需要我们自定义一个类,继承Thread,并复写run方法,而这个子类的run方法中的内容就是我们指定其将要执行的代码。

        关于上述内容,这里我们可以通过Thread类的源代码进行说明。

代码2:

//Thread类run方法源代码@Overridepublic voidrun(){       if (target != null) {target.run();}}
由以上代码可知,Thread类的run方法非常简单,如果target变量不为空则那么该线程所要执行的代码就是target变量所指向对象(Runnable接口实现类对象,下面会讲到)的run方法,否则什么都不执行。这也就是为什么需要复写run方法的原因——若直接调用Thread对象的run方法不会有任何执行效果。有关Runnable接口实现类对象涉及到创建的线程的第二种方法,因此将在后面的内容中介绍。

        关于第三步:可能大家会对调用线程对象的start方法而不调用run方法有所疑惑。查阅Thread类的API文档,其中对start方法的说明为:使该线程开始执行;Java虚拟机调用该线程的run方法。也就是说,通过new关键字仅仅是创建了一个线程对象,但该对象还并未作为一个线程运行,换句话说如果代码2中我们调用的是SubThread对象的run方法,那么这与调用一个对象的普通方法没有区别——因为这是单线程的;只有调用该对象的start方法才使该线程作为主线程以外的线程独立运行起来,并执行定义在run方法中的指定代码。

2) 多线程的运行

为了演示多线程的运行效果,我们对代码1做一些改动,改动如下,

代码3:

//定义线程类需要继承Thread类,并复写run方法class SubThread extends Thread{//复写run方法public void run(){//循环打印for(int x = 0; x<50; x++)System.out.println("SubThread----------"+x);}}public class ThreadDemo2{public static void main(String[] args){//创建线程,并执行其中的run方法。SubThread st = new SubThread();st.run(); //主线程也执行一段循环打印代码for(int x = 0; x<50; x++)System.out.println("PrimeThread-----"+x);}}
运行结果内容过多,不再这里显示了,理论上应是主线程和子线程交替打印,如下所示:

SubThread----------0

PrimeThread-----0

SubThread----------1

PrimeThread-----1

 

我们通过下图来理解代码2的执行过程, 

 

        我们将代码2执行结果和上图结合起来:当主线程执行到“SubThreadst = new SubThread();”语句创建了一个子线程,然后调用子线程的start方法启动子线程。子线程启动以后,主线程继续“向下执行”for循环,同时子线程也执行自己的代码——for循环,因此从控制台的结果上看,两个线程在同时运行。此外,两个线程的打印结果是交替出现的(并不完全是),这是因为,CPU不仅在多个进程之间进行快速地切换,更具体来说,CPU在单个进程内又在多个线程之间进行快速切换,因此,从结果上看就是——执行一会儿主线程输出语句,再执行一会儿子线程输出语句,这样不断地进行交替。

        如果多次运行代码2,就会发现,每次运行的结果是不同的。这是因为,CPU在线程间进行切换时,具体更多地执行哪个线程是由CPU随机决定的,这也就体现了多线程的一个特点:随机性。

3) 多线程练习

        为了巩固创建和执行线程的过程,我们做一个简单的练习。

需求:创建两个线程,和主线程交替运行。

分析一:定义Thread类的子类,并复写run方法——循环语句;在主函数中创建两个线程对象,分别调用其start方法,接着在主函数中执行循环语句,最终在控制台中呈现连个子线程与主线程同时执行的效果,并交替打印指定内容。

代码:

代码4:

//定义Thread类的子类class SubThread extends Thread{//为方便区分两个子类为每个线程对象定义名称private String name;SubThread(String name){this.name = name;}public void run(){//循环输出for(int x=0;x<50; x++)System.out.println(name+"------"+x);}}public class ThreadTest{public static void main(String[] args){// 主函数中创建两个线程对象,分别调用start方法SubThread st1 = newSubThread("No.1");SubThread st2 = newSubThread("No.2");             st1.start();st2.start();             //在主函数中同样执行输出循环,以呈现与子线程同时运行的效果for(int x=0;x<50; x++)System.out.println("MainThread----------"+x);}}
注意:这里我们还是要强调,创建线程对象以后,一定要调用其start方法,而不是run方法,否则运行效果与单线程是没有区别的——st1指向的线程对象执行完其run方法中的代码之前其他线程无法运行。 

分析二:实际上为了方便区分多个线程,Thread类中已经定义了私有成员变量name,以及获取线程名称的方法getName(),因此可以在对象内部通过“this.getName()”语句获取到当前正在执行的线程对象的默认名称。当然,如有需要也可以通过setName()方法设置自定义线程名称。不过,如果想获取到主线程对象的引用就不能使用this关键了,因为执行main方法的主线程对象并非是main方法所在类的实例对象,为此Thread类还对外提供了一个静态方法,专门用于返回指向当前线程对象的引用,包括主线程——currentThread()。那么最终就可以通过“Thread.currentThread().getName()”语句来获取主线程名称。该方法对于普通线程对象的作用等同于“this”。

代码:

代码5:

//对代码1进行简单的修改public class SubThread extends Thread{//由于Thread类已经定义了name成员变量,不必重复定义//private String name;//方便创建默认名称线程而手动定义空参构造方法SubThread(){}SubThread(String name){//Thread类中已经定义了name的初始化动作,通过super()调用即可super(name);}public void run(){//循环输出for(int x=0; x<50; x++)//通过this获取当前线程对象的引用System.out.println(this.getName()+"---"+x);}}public classThreadTest2{       public static void main(String[] args){SubThread st1 = new SubThread();SubThread st2 = new SubThread();st1.start();st2.start();             for(int x = 0; x<50; x++)//通过currentThread获取对于主线程的引用System.out.println(Thread.currentThread().getName()+"----------"+x);}}

        虽然通过this关键字可以获取到手动创建线程对象的引用,但是并不推荐在实际开发时这样做。一方面,主线程对象是无法通过这一方法获取到的,如果使用this关键字习惯了就容易出错;另一方面,当我们通过第二种方式创建线程时,也不能通过this关键字获取当前线程对象的引用(下面会讲到),体现了this关键的局限性。

2. 线程的状态

1) 线程正常运行的前提条件

        一个线程的正常运行需要同时满足两个条件:(a) 具有被CPU处理的资格;(b) 同时还要具有执行权,否则,即使线程被创建并被调用start方法也是不能运行的。这两个条件之间的关系是:只有满足(a)才能有可能满足(b),也就是说不满足(a),肯定不满足(b)。资格是线程运行的根本前提条件,这是由人为控制的,如果不需要该线程运行,就通过调用一些方法来“剥夺”其资格;然而,仅具有资格是不够的,还需要满足直接前提条件——执行权。执行权是由CPU分配的,当有多个线程具有资格时,CPU执行哪个线程,该线程就具备执行权,从而真正运行起来。

了解了上述两个前提条件,我们就来说一说除创建和运行以外,线程其他的三个状态。

2) 状态一:冻结

条件:不具备资格;不具备执行权。

说明:暂停线程的运行,但该线程并未“死亡”或者“消失”。换句话说,当条件满足的时候——达到规定时间或者人为唤醒——该线程可以被人为赋予资格,并在CPU赋予其执行权后继续运行。

实现方式:

方法一:调用sleep(long millis)方法,并指定冻结时长。当达到规定时长以后,线程自动唤醒,并继续运行。

方法二:调用wait()方法,使运行中的线程冻结。使用wait方法冻结的线程是无法自动唤醒的(只限于无参wait重载方法),只能手动唤醒该线程——通过调用该线程的notify()方法,notify的意思就是唤醒。这里要提一下,如果某个进程中的线程一直处于等待状态,那么这个进程就不会结束。

 

小知识点1:

我们经常会通过任务管理器中的“结束进程”按钮,来强制关闭某个卡死的进程。实际上,这个按钮的作用就是强制“消灭”该进程中所有正在运行的线程,以达到结束进程的目的。

 

3) 状态二:消亡

条件:不具备资格,不具备执行权。

说明:顾名思义,就是结束该线程。被“结束”的线程等同于没有指向的对象,是不能再被“唤醒”的。当然,处于“冻结”状态的线程也是可以被“杀死”,而消亡的。

实现方式:

方法一:调用stop()方法,在线程运行过程中强制使其消亡。

方法二:线程执行完run方法中的代码,自行消亡。

4) 状态三:阻塞(临时)

条件:具备资格,但不具备执行权。

说明:举个例子,当在主函数中创建了四个子线程,并分别调用它们的start方法,启动这些线程,但是在某一个时刻,真正在运行的线程其实只有一个(因为CPU只能处理一个),那么在一个时刻,其他三个线程就处于阻塞状态——具有被CPU处理的资格,但是没有执行权。换句话说,只有某个线程被赋予了执行权(通常由CPU决定)才能真正被运行起来。

方法:该状态无法人为控制,CPU分配。


我们通过下图来加深对线程各个状态及其相互关系的理解,


3. 定义线程类的第二种方法

在前面的内容中我们说明了定义线程类的第一种方法:继承Thread类,现在我们说一说定义线程类的第二种方法,并通过一个小例子来体现这种方法的应用。

1) 定义方式

        我们还是回过头来看一看Thread类的API文档,其中提到了定义线程类的第二种方法:定义一个实现Runnable接口的类,并复写该接口的的run方法。然后创建该类的实例对象,最后在创建一个Thread对象时将该对象作为一个参数来传递,并通过调用Thread对象的start方法来启动这个线程。下面是通过这种方法定义、创建和启动线程的代码格式:

定义线程类的代码格式:

代码6:

//实现Runnable接口class PrimeRun implements Runnable{//复写run方法public void run(){//自定义需要子线程执行的代码}}
创建并启动线程的代码格式:

代码7:

//创建代码3中定义的PrimeRun类对象PrimeRun p = new PrimeRun();//创建Thread对象,并将PrimeRun对象作为参数传入,然后启动该线程new Thread(p).start();

总结起来分为五个步骤:

第一步:定义实现Runnable接口的子类;

第二步:覆盖Runnable接口中的run方法;

第三步:创建Thread对象,一个Thread对象表示一个线程;

第四步:将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法——Thread(Runnable target);

第五步:调用Thread对象的start放阿飞开启线程,并执行Runnable子类对象run方法中的代码。

        此时我们再来看Thread类源代码中的run方法(代码2),其中的target变量就是指向的传递到Thread对象中的Runnable实现类对象,那么当该变量不为空时,启动线程就将执行Runnable实现类对象的run方法。实际上targetThread类的一个Runnable类型的私有成员变量,在通过Thread类的构造方法创建Thread对象时,将在构造方法中调用一个名为init的私有方法,该方法的作用之一就是接收传递到Thread构造方法的Runnable实现类对象,并将其赋值给target成员变量。更为详细的原理大家可以参考Thread类的源代码。

        那么当我们通过为Thread对象初始化一个Runnable实现类对象的方式创建一个线程时,能否在Runnable实现类的run方法中通过this关键字获取当前线程对象的引用呢?答案是否定的,因为此时的this指向的是Runnable实现类对象,而非Thread对象,因此此时只能通过调用currentThread方法。

2) 演示示例

需求:简单的卖票程序。

分析:首先,通常在火车站的售票大厅有多个售票窗口在同时卖票,以此来提高卖票效率。这里我们假设只有4个卖票窗口,那么对应的就是4个线程。其次,我们应该定义一个资源类——“车票”,并创建车票类对象,该对象内部定义了车票总数。然后将车票对象传入卖票窗口内,最终4个窗口同时对其进行循环减法运算——卖票。这么做可以使得4个窗口共享一个车票资源,而不会导致重票的产生。

实现方式:定义实现了Runnable接口的Tickets类,内部定义表示车票总数的成员变量count初始化值为100,并复写run方法——当count大于0时进行循环减法——模拟卖票过程。在主函数内创建一个Tickets对象,并在创建4个Thread对象时将Tickets对象作为参数传递。最后分别开启4个线程。

代码:

代码8:

//定义实现Runnable接口的Tickets类public class Tickets implements Runnable{    //定义车票总数private int count = 100;      //复写run方法public void run(){//对车票数进行循环减法,直到车票数为0while(true){if(count > 0)//打印线程名称和当前票数System.out.println(Thread.currentThread().getName()+"-----"+count--);}}}public class ThreadDemo3{public static void main(String[] args){Tickets ts = new Tickets();             //创建4个线程对象,并将Tickets对象作为参数进行传递Thread t1 = new Thread(ts);Thread t2 = new Thread(ts);Thread t3 = new Thread(ts);Thread t4 = new Thread(ts);             t1.start();t2.start();t3.start();t4.start();}}
补充:

        在上述分析部分中,我们提到了共享车票资源避免重票的产生。可能有朋友会想到另一种方法:定义Tickets类的时候还是继承Thread类,并在该类内部定义个一个静态成员变量表示车票总数,这样也可以做到多个Tickets对象共享车票数据的目的。但是这样做的后果就是导致数据的生命周期过长,不利于内存的优化。因此不建议使用这一方法。

注意:

(a)    实现Runnable接口的类并不是线程类,因为它与Thread类不存在继承关系。在任何情况下,只有Thread类及其子类对象才是线程对象。

(b)    上述方法中我们将需要子线程执行的代码定义在了Runnable子类的run方法总,而不是Thread子类,因此应该使得Runnable子类对象和Thread对象之间产生一定的联系——通过Thread类的Thread(Runnable target)构造方法,多态地接受Runnable接口的子类对象,这也就是为什么要做前述第四步的原因。产生了这一联系以后在调用Thread对象的start方法时,首先执行Thread对象的run方法,该run方法最终又去执行了Runnable子类对象的run方法。

(c)    关于上面提到的Runnable接口,其API文档中有这样的说明:大多数情况下,如果只想重写run方法,而不重写其他Thread方法,那么应使用Runnable接口。这很重要,因为除非程序员打算修改或增强Thread类的基本行为,否则不应为该类创建子类。所以,从实际开发的角度来说,通过实现Runnable接口的方式定义线程类的方式更为常用,大家应重点掌握。

3) 两种定义线程类方法的区别——Runnable的设计目的

        区别一:这里最关键的问题是——Java语法中不允许进行直接的多继承。举个例子,如果一个类声明为Thread类的子类,那么它就不能再继承其他类了,这在很大程度上限制了该类在后期功能上的扩展。从另一个角度来说,假如某个类是通过继承其他类来定义的,那么它就不能再去继承Thread类而使用多线程技术了,同样不便于功能扩展。因此,设计Runnable接口的目的就是为了提高代码的扩展性,这也是为什么实际开发中更多地使用这一方法的原因。

        区别二:两种方法中,需要子线程单独执行的代码的存放位置不同。继承Thread类:直接存储在Thread子类run方法中;实现Runnable接口:存放在Runnable接口子类run方法中。那么通过后者,更能体现面向对象的思想——将某个线程将要执行的代码专门存储到一个对象中,降低了线程与执行代码之间的耦合性。
如果同时使用创建线程的两种方法创建一个线程——不仅定义Thread类的子类复写run方法,而且定义Runnable接口的实现类复写run方法,那么启动线程后究竟执行那个run方法的代码呢?先阅读以下代码。

代码9:

new Thread(new Runnable() {//Runnable实现类的run方法@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Runnable :"+Thread.currentThread().getName() + " : "+Calendar.getInstance().get(Calendar.SECOND));}}           }){//Thread子类的run方法@Overridepublic void run(){for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread :"+Thread.currentThread().getName() + " : "+Calendar.getInstance().get(Calendar.SECOND));}}}.start();
执行结果为:

Thread :Thread-0 : 2

Thread :Thread-0 : 3

Thread :Thread-0 : 4

Thread :Thread-0 : 5

Thread :Thread-0 : 6

Thread :Thread-0 : 7

Thread :Thread-0 : 8

Thread :Thread-0 : 9

Thread :Thread-0 : 10

Thread :Thread-0 : 11

代码说明:

(1)  以上代码中无论是Thread类的子类对象,还是Runnable实现类对象,均通过匿名内部类的方式创建。

(2)  两者run方法中执行的代码是一样的——每秒打印当前时间的秒值大小,共打印十秒钟。

(3)  从执行结果来看,执行的是Thread子类中的run方法。这是因为Thread类的子类直接复写了父类的run方法,因此也就不会进行target变量是否为空的判断,以及调用target对象的run方法的操作,而是直接执行子类run方法的内容。

4. 实际开发中的简单应用

如果某个程序需要处理多个数据,并且相互之间是独立的,可以将匿名内部类和多线程结合起来实现功能,不仅便于代码书写,而且可以提高程序运行效率。举个例子,代码如下,

代码10:

class ThreadDemo4{public static void main(String[] args){//Thread类的子类对象newThread(){public void run(){//定义数据处理代码,这里仅以循环作为演示for(int x = 0; x<50; x++)System.out.println(Thread.currentThread().getName()+"-----"+x);}}.start(); //Runnable接口的子类对象Runnable run = new Runnable(){publicv oid run(){//定义数据处理代码,这里仅以循环作为演示for(int x = 0; x<50; x++)System.out.println(Thread.currentThread().getName()+"-----"+x);}};new Thread(run).start();             //主函数中再单独处理一部分数据for(int x = 0; x<50; x++)System.out.println(Thread.currentThread().getName()+"-----"+x);}}
上述代码中通过匿名内部类的方式,分别采用两种不同的线程定义方法,实现了在主函数内同时处理3部分数据的功能。

0 0
原创粉丝点击