多线程、I/O、JCF 号称 JavaSE 中的“三座大山”,其中 I/O 和 JCF 只要我们平时多加练习 API 的使用,熟练的使用并掌握并不是特别难的事情。但唯独多线程是既需要多加练习,也需要仔细思考理解的,可谓是“三座大山”中的最高峰。本系列博文将详细讲解 JavaSE 多线程部分的知识。特别说明,作者也是边学习边写作博文,若文中有不准确或不正确的地方,万望大家积极留言指正,不胜感激。
对于线程的基本知识我们可以在任意一本讲解操作系统的书上找到定义和解释,作者只在这里简要进行介绍以供后文讲解技术实现做基础。
在了解线程之前,我们首先需要了解进程。打开 windows 的任务管理器或者在 Linux 系统下执行 top 命令,我们都可以看到操作系统当前正在运行的进程。我们可以把进程看作是一个程序的运行实体,也就是说当一个进程存在时,他所对应的应用程序必然是启动的。进程作为操作系统分配资源的最小单位,其独立拥有一块内存空间,而我们可以在这个独立的内存空间里创建线程。
特别说明,操作系统在同一时刻只可能运行一个程序,即在同一时刻只有一个进程是处于运行状态。虽然在我们日常使用中,即可以一边写文档,一边听音乐,但这并不表示操作系统在同一时刻既运行着Office,也运行着播放器。进程的执行需要CPU的支持,只有获得了CPU使用权的进程才可以执行。只是因为现在的操作系统都是“多道操作系统”,主存中可以同时存在多个进程,他们在管理程序的控制下轮流使用CPU,并且切换的速度快到让人无法察觉,所以会对我们造成同个程序在同时执行的错觉。
线程是存在于进程内部的“微型进程”,线程不能够脱离进程存在,一个进程的关闭必然导致其内部的所有线程被关闭。进程内部可以拥有多个线程(数量由这个进程所占有的内存空间大小决定),这些线程共享进程的内存空间和内存空间内的资源。进程内部所有线程的执行结果就是整个进程的执行结果,线程可以将进程的庞大任务拆解开来执行,提高执行效率。所以我们可以把进程看作是一个产品流水线,而线程就是这个流水线上的各个环节,他们协同完成产品的制造。
进程的内存空间中有些资源是只能在同一时刻仅能被一个线程所使用的,这种资源我们称作“临界资源”,而当我们拿到“临界资源”后接下来要做的事情,我们就称作“临界区”。对于“临界资源”和“临界区”的概念请大家牢记,我们在下面的讲解中会经常提到这两个概念。其实在进程之间也有“临界资源”和“临界区”的概念,比如打印机就是一个“临界资源”,他在同一时刻只能接受一台计算机发出的打印命令,而接到打印命令后进行打印的整个过程就是“临界区”。
在 Java 中我们有两种方式可以创建一个线程,第一种是继承自 java.lang.Thread 类并重写 run() 方法,示例如下:
public class ByThread { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } /** * 自定义线程 * 继承自 java.lang.Thread 类并重写其 run() 方法定义线程任务 * @author leon.gan */ class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("====线程任务===="); } } }
我们定义一个类 MyThread 使其继承 java.lang.Thread 类,这样我们定义的类就成为了一个线程类。我们创建线程,是为了让线程去完成我们指定的任务,所以我们需要重写 run() 方法,将线程需要完成的事情要定义在 run() 方法内,也就是说,只有在 run() 方法内的代码才会被线程所执行。
在我们创建了线程的实例后,调用实例方法 start() 来启动线程,让线程执行我们定义的线程任务。特别注意,启动线程调用的是 start() 方法,而不是 run() 方法。如果我们使用线程实例来调用 run() 方法,那和调用一个普通方法没有区别,是绝对无法启动线程的。
创建线程的第二种方法是实现 java.lang.Runnable 接口,同样需要重写 run() 方法,示例如下:
public class ByRunnable { public static void main(String[] args) { MyThread2 mt = new MyThread2(); // 实现自 java.lang.Runnable 接口的自定义线程必须依赖 java.lang.Thread 来启动 Thread t1 = new Thread(mt); t1.start(); } } /** * 自定义线程 * 实现 java.lang.Runnable 接口并重写其 run() 方法定义线程任务 * @author leon.gan */ class MyThread2 implements Runnable { @Override public void run() { while (true) { System.out.println("====线程任务===="); } } }
我们定义一个类 MyThread2 使其实现 java.lang.Runnable 接口并重写 run() 方法定义线程任务。从上面的示例代码可以看出,实现自 java.lang.Runnable 接口的线程类的实例并不能调用 start() 方法进行启动。观察 java.lang.Runnable 接口的源码可以发现,这个接口仅仅定义了一个 run() 方法,
public abstract void run();
再观察 java.lang.Thread 类的源码,我们发现其实际上本身就是 java.lang.Runnable 接口的实现类,
public class Thread implements Runnable
而对线程进行基本控制的方法都是在 java.lang.Thread 里面定义的。java.lang.Thread 类提供了一个接受 java.lang.Runnable 实现类实例对象参数的构造方法,
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
我们将自定义的线程类 MyThread2 的实例传入即可成功创建可以启动和操作的线程对象。
那么我们在实际创建线程时应该使用哪种方法呢?我们推荐采用实现 java.lang.Runnable 接口的方式来创建线程。首先,Java 的语法规定类只能继承自一个直接父类,但是可以实现多个接口,所以既然能用接口来创建线程,我们就很有必要将宝贵的父类位置留给更需要的情况。其次,采用实现 java.lang.Runnable 接口的方式更利于实现资源共享,具体资源共享大家可以查找车站售票的例子来理解,这里就不浪费字数了。特别说明,为了编写示例代码的方便,文中出现的大部分示例都采用了继承 java.lang.Thread 类创建线程的方法,但这并不代表作者支持在平时开发中使用这种方式来创建线程。
总的来看,继承 java.lang.Thread 类来创建线程,更多的是要表达“多个线程分别完成自己的任务”的意思,而实现 java.lang.Runnable 接口来创建线程,表达的是“多个线程协同完成一个任务”的意思。