第七课JDK的使用2

2. 线程

  我们所开发的程序在运行的时候被称为一个进程Process,大家可以打开Windows的任务管理器,就可以看到当前系统正在运行的进程列表。

而一个进程中至少会启动一个线程Thread,具体的功能是在线程中完成的。每个线程内部的代码是顺序执行的,而线程之间则是并行的。线程是程序开发的重要概念,许多资源的使用会启动新的线程。这是因为资源的响应需要时间,如果主线程一直等待则界面会处在一种卡死的状态,不会响应操作者的指令。如果在使用某种资源时启动一个新线程,由它来负责与资源进行互动,主线程则继续做其它的任务。当资源实现了我们想要的结果之后,再由分线程通知主线程,则程序的运行就对操作者显得友好多了。这就是使用线程的意义所在,而且有许多使用资源的类本身也就要求使用者启动线程才能调用。

  一个线程在生命周期中可能处于5种状态,第一是新建,即线程对象被new语句创建出来。第二是就绪,线程对象的start方法被调用,该对象会被加入到系统的线程调度器上,等待被分配CUP资源。第三是运行,线程获得CPU资源,该对象的run方法会被调用,开始执行线程被赋予的任务。第四是阻塞,如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,会让出所占用资源进入阻塞状态。在睡眠时间已到或获得设备资源后重新进入就绪状态。第五是终止,一个运行状态的线程完成任务或者其它终止条件发生时,该线程就切换到终止状态。5种状态之间的切换过程如下图所示:

  线程的知识点主要包括如何创建及启动线程,线程之间如何通信,如何避免线程间争抢资源。要创建一个线程,可以通过实现Runnable接口或继承Thread类两种方式进行。Thread类是一个抽象类必须要被继承才能实例化,这个类本身也实现了Runnable接口,对接口的一些方法有了实现,因而使用简单一些。我们以继承Thread类的方式来创建线程。我们在ExampleUnitTest中敲入如下代码:

    Thread thread = new Thread() {        @Override        public void run() {            for (int i = 0; i < 30; i++) {                System.out.print(i + "分 ");  // 分线程中输出的字符带“分”标识                if ((i + 1) % 15 == 0)    // 每15个换行                    System.out.print("\n");            }        }    };    thread.start();        for (int i = 0; i < 30; i++) {        System.out.print(i + "主 ");    // 主线程中输出的字符带“主”标识        if ((i + 1) % 15 == 0)      // 每15个换行            System.out.print("\n");    }

  这段代码中,我们用匿名类来继承Thread类,并在其中重写了run方法,这是线程进入运行状态后真正干活的部分,这里是输出30个数字。之后我们通过调用thread对象的start方法使其进入就绪状态,如果调度器分给它CUP资源,它的run方法就会被调用进入运行状态。之后的代码是在主线程中同样输出30个数字。代码运行结果如下:

  运行结果可以看出主线程与分线程的输出是混在一起的,也就是说它们是并发运行的。因为线程运行的并发性,因此这段代码每次执行的结果也会不一样。

  如果我们把上一段代码中分线程中输出数字的次数改为一万次,而在主线程中则不再输出,我们再看一下运行结果:

  同学们可以看到,并没有像我们想要的那样执行一万次输出,而是到二百多次就结束了。这是因为主线程没有耗费同样时长的执行代码,主线程结束导致进程结束,进程结束时一并终止了分线程。

  因为线程的执行是并发进行的,如果不同线程对同一个资源进行操作时就有可能造成混乱。比如一个线程正在运算时,它所使用的一个对象的成员变量值被另一个线程改变了。要避免这种情况,一是可以使用“锁”机制,使一个线程在使用资源时将资源锁住不让其它线程使用。“锁”机制我们不讲,同学们在以后实践中遇到了再找资料学习,知道了它是干什么的理解起来也不难。另一种办法是使用线程间发送消息的方式。某个资源只由一个线程处理,当其它线程要改变该资源时,就向资源的管理线程发送消息,再由该管理线程来对资源进行操作。这种办法是很常用的,最常用到的是对程序界面的操作上。在Android平台中,界面操作只能在主线程中进行。线程间发送消息需要借助Handler类的对象(这个类是在android.os包中),我们需要继承Handler类并重写其handleMessage方法,在主线程中实例化该类对象,在分线程中调用该对象sendMessage发送消息。我们在MainActivity中敲入如下示例代码,实现在文本中每隔1秒刷新显示当前时间:

import android.widget.TextView;import java.util.Date;import android.os.*;
public class MainActivity extends AppCompatActivity { private TextView txtResult;
private Handler handler = new Handler(){ // 匿名类继承Handler类并重写其handleMessage方法,并创建对象 @Override public void handleMessage(Message msg) { switch (msg.what){ case 200: txtResult.setText(String.format("%tT", Calendar.getInstance())); //在主线程中对文本进行操作,显示当前时间 break; } } };
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
txtResult = (TextView)findViewById(R.id.txtResult);
new Thread(){ //匿名类继承Thread类重写run方法,并创建对象 @Override public void run() { while (true){ Message msg = new Message(); msg.what = 200; // 约定的消息代码 handler.sendMessage(msg); // 通过Handler对象向主线程发送消息 try { sleep(1000); // 休眠1000毫秒 } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); }}

  代码运行以后,界面显示如下所示:

  在上面的代码中Handler的对象是成员变量,因此线程中可以直接使用,当然也可以是局部变量通过线程构造函数传给线程。在线程的run方法中,创建了一个Message类的对象(这也是一个android.os包中的类),这是要发送消息的载体,示例里给这个对象的what变量赋值200,这是约定的消息代码。再调用Handler对象sendMessage方法把它发送出去。线程发送消息后并不会等待接收消息的线程的处理,而是继承运行余下的代码,即休眠1秒后再次发送消息。主线程接收到消息后,Handler对象中被重写的handleMessage方法被调用,在其中完成对界面上时间的刷新。

  线程里的概念比较多,理解起来也难一些。但对于初学者来说,知道如何继承Thread类来创建并启动线程,Android开发中不要在线程中对界面进行操作以及在接触一些资源时知道这是要通过启动线程才能使用的就可以了。大家多试着敲几遍给出的示例就对线程有基本的了解,更复杂的东西等实践的时候遇到再学也来得及。

3. 异常

  上面我们在实现时钟功能时,在线程中有一段奇怪的代码:

    try {        sleep(1000);    // 休眠1000毫秒    } catch (InterruptedException e) {        e.printStackTrace();    }

  这是对异常进行捕获和处理的代码。异常(Exception)是程序运行中的一些错误,这些被称为异常的错误一般是由于两种原因引起的。一种是由于逻辑原因引起的异常,比如在执行除数时除数的值是0,访问数组时序号超出范围(也称为越界)等。这类异常编译时不会发现问题,只会在运行时出现问题,被称为运行时异常Runtime Exception。另一种是访问资源时资源有可能出错引发的异常,比如访问文件时文件不存在引发的异常。这类资源的封装类供调用的方法在声明的就会说明可能出现的异常,而且在调用该方法时必须对异常进行捕获与处理。比如上面代码里用到的Thread类的sleep方法,它的声明是:

public static void sleep(long millis) throws InterruptedException

这里明确说了可能抛出InterruptedException异常。这样的方法在调用时,必须放在try…catch代码块里,否则编译时不能通过。这一类异常被称为可检查异常Checked Exception。

  我们看一下java.lang中的异常类继承关系图。

  异常都被封装在继承自Exception的类里,Exception是所有异常的基类,而RuntimeException则是所有运行时异常的基类,在其子类中可以看到由除0引起的ArithmeticException(算术异常),数组序号越界引起的IndexOutOfBoundsException。除了RuntimeException及其子类之外的异常都是可检查异常,其中就有我们上面代码中用到的InterruptedException。

  异常的处理是由try…catch…finally代码块来完成的。使用的基本方式如下:

    try {       // 有可能出现异常的代码    }catch(ExceptionType e) {       // 对捕获的异常进行处理的代码    }finally{       // 最终完成代码    }

  将可能出现异常的语句放在try块里(一般都是要使用的方法声明了可能会抛出的异常),catch后的括号是要捕获的异常的类型,如果异常类型用的是Exception,这样就会捕获所有可能的异常,因为所有异常都是它的子类。如果try块的语句有可能抛出几个异常,则可以叠加多个catch语句来分别捕获。如下面示例:

    try {       // 有可能出现异常的代码    }catch(ExceptionType1 e) {       // 对捕获的异常进行处理的代码    }catch(ExceptionType2 e) {       // 对捕获的异常进行处理的代码    }catch(ExceptionType3 e) {       // 对捕获的异常进行处理的代码    }

  捕获的异常对象,即catch括号里声明的对象包括当前发生的异常的信息,可以根据提取的信息来处理,如记录到日志文件中或向程序操作者弹出提示消息。finally块中放的是try…catch结束后运行的代码,在这里的代码不论是否发生异常都会被执行。try块里的代码如果有异常发生,会跳转到catch块中运行,后续的代码不会被执行。而当没有异常发生时,catch块中的代码也不会运行。但是不管有没有异常发生,finally块中的代码都会运行。

  我们在代码中不仅可以捕获异常,还可以抛出异常。我们可以在自己的方法里创建一个已有异常类的对象,用throw语句把它抛出去。也可以继承一个异常类来创建自己的异常类,抛出自己异常类的对象。比如下列代码:

public class className{   public void doSth() throws MyException{       // ...      throw new MyException(); // 抛出自定义的异常类对象   }}

  这段代码中抛出了自定义的MyException类的异常对象,有抛出异常的方法需要在声明中用throws关键词明确会抛出的异常类型。

  程序中出现的错误中,异常是可以在程序中进行捕获并进行相应处理的。其它的错误则不可预料,也无法处理,如系统内存耗尽引起的错误。这种程序没有能力处理也不用处理的错误被封装在Error类的子类中,大家知道就可以了。


第一季 零基础学习Android开发

第一课 第一个Android程序(1)

第一课 第一个Android程序(2)

第二课 Java语言基础1(1)

第二课 Java语言基础1(2)

第三课 Java语言基础2-1

第三课 Java语言基础2-2

第四课 Java语言基础3-1

第四课 Java语言基础3-2

第五课 类与面向对象编程1

第五课 类与面向对象编程1-2

第五课 类与面向对象编程1-3

第六课 类与面向对象编程2-1

第六课 类与面向对象编程2-2

第六课 类与面向对象编程2-3


文章转载自微信公众号:跟陶叔学编程

类似文章