黑马程序员——多线程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方法。实际上target是Thread类的一个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部分数据的功能。
- 黑马程序员——多线程2:应用
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- 黑马程序员—多线程
- QT 官方实例 学习
- unity学习之综合题的解析
- Sublime Text 3 使用
- Unity3D之for题目
- "Android SDK Content loader has encountered a problem parseSdkContent Failed"
- 黑马程序员——多线程2:应用
- VirtualBox内Linux系统怎样与Windows共享文件夹
- HDU_4001_To miss our children time_动态规划
- 循环综合题分析
- IHERB上待产包准备指南-妈妈篇
- 队列(queue) 之 c++模板实现(友元函数和运算符重载)
- javascript的总结
- 关于掩码
- POJ 2516 Minimum Cost (最小费用最大流)