进程与线程
- 什么是进程: 我们先看看比较官方的表述,进程是操作系统结构的基础,是一次程序的执行,是一个程序及其数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
其实关于进程的官方定义也是很好懂,但是我们都不太喜欢过于官方的东西,还是来点比较实在解释比较好。进程就是我们打开的一个个应用程序,每一个应用程序都有属于自己的一个端口号,操作系统为每一个应用程序分配内存空间后,端口号成为了应用程序在任务队列的标识(就好比是程序在内存中的地址,独一无二),只要应用程序没有被关闭,操作系统没有回收应用程序所占的空间,则程序一直占有内存。这也就是为什么有些时候打开应用程序时我们会遇到端口已经被占用的问题(最好的例子,你在IDE中打开了Tomcat,那么在外部就不能重复打开),因为在同一个时刻内只能有一个应用程序独占同一段内存空间,这就是进程。再通俗一点,就是任务管理器里面的进程队列:
- 什么是线程: 刚开始听到线程的时候可能会感觉有点懵,脑袋有点转不过弯来,就拿学习Java的过程来说吧,刚开始学习啥是面向对象啊,异常啊,泛型啊,集合啊什么的,虽然开始接触时会有点难以理解,但是慢慢的,随着学习的深入,总是有迹可循的。但是线程这部分知识给我的感觉很独特,那就是学了感觉懂一点,但是总觉得抓不住精髓,所以往往是无从下手。后来,好好磨了一下这部分知识,感觉抓到一点头绪了,现在我就说说我眼中的线程:前面介绍了进程,我们知道了进程就好比一个个的应用程序,我们可以同时打开多个应用程序,边听歌边打游戏,操作系统则对这些应用进行调度,对于这一个个的进程,操作系统就是掌握他们生杀大权的上帝。理解了这个,我们来理解线程就会很容易了,线程就好比进程中一个个独立的子任务,进程就好比操作系统,是所有线程的上帝。以次类推,在一个进程中,就拿QQ来说,我们可以边聊天,边下载QQ文件,边发送表情,这些功能就好比一个个的线程,并同时在后台默默运行,应用程序则进行总的调度。
补充一点:在开始学习Java的时候(或者是C, C++等),我们都知道程序的入口是main,其实学习了线程后我们将了解到,main其实也是一个线程,它有一个特殊的名字:主线程(main线程),所以说,我们原先编写的程序大多是单线程的程序(只用到了主线程)。而Java中的多线程的编程,就是为了让我们掌握关于多任务多功能并行处理的能力,现在我们就进入多线程编程之旅。
创建线程的四种方式
在我们没学习多线程之前,main线程就是默认线程,程序的所有运算都在主线程上执行,main线程是为我们提供好的程序入口(main方法),不需要我们去创建。但假如我们需要其他的子线程去帮助我们执行其他的任务,那么就需要创建新的线程,下面就介绍四种创建线程的方式:
第一种——继承Thread类
- Thread类简介: Thread类是JDK1.0时就提供的一个线程类,它为Java提供了多线程编程的能力,每一个Java类只需要继承Thread类,并重写其中的run()方法,那么该类就是一个新的子线程。程序示例如下:
1 | public class MyThread extends Thread { |
第二种——实现Runnable接口
继承Thread类固然是一种简单的创建线程的方式,但是由于Java中的单继承机制,使得继承了Thread类就不能再继承其他的类,这是不利于扩展的。所以可以使用另一种方式,实现Runnable接口。Runnable接口是一个公共协议,里面只提供了一个run()方法。其实,Thread类也同样实现了Runnable接口,所以通过继承Thread类得到线程和实现Runnable接口得到线程没有区别,只是实现Runnable接口更容易扩展。程序示例如下:
1 | /** |
第三种——实现Callable接口
上面的两种创建线程方式是我们比较熟悉的,其实一直到JDK 1.5之前,创建线程的也只有上面的两种。在JDK 1.5 之后,Java对线程做出了更加丰富的扩充,添加了一个java.util.concurrent的包(多线程编程者的福音),里面提供了更多更加丰富,功能更加完善的方法,其中在创建线程方面也做了扩充,而实现Callable接口创建线程就是其中一种方法。下面是代码示例:
1 | public class ThreadDemo implements Callable<Integer> { |
通过对比实现Runnable和实现Callable,我们会发现比较有趣的地方:
- 实现Runnable接口,run()方法没有返回值,且run()方法不能抛出异常
- 实现Callable接口,call()方法有返回值,且可以抛出异常
其实这两个方法就设计的就很有意思,run翻译过来就是执行的意思,也就是说我给你一个任务,你只要负责把它做完,其他的不用你操心,做完了也不用告诉我(不叫你哔哔你就别哔哔)。而call翻译过来有打电话的意思,打电话肯定要有回音啊(你必须哔哔),而且占线了(对不起,电话暂时无法接通…)是不是要说一声啊(抛异常)。
第四种——通过线程池来创建线程
其实上面提供的几种创建线程的方法在项目开发中是不常用的,因为假如出现这种情况:如果并发的线程数量特别多,并且每个线程只执行很短的时间就被销毁,这样频繁的创建和销毁线程会大大降低系统的效率。在JDK 1.5之前遇到这样的问题我们也只能干瞪眼,但是在JDK 1.5之后,我们却有能力对这种情况作出改善,那就是创建一个线程池(Executor, java.util.concurrent包下的Executor接口),下面只介绍如何通过线程池去创建线程,关于线程池的详细构造与说明请看另一篇笔记。下面是程序示例:
1 | public class ThreadA implements Runnable { |
其实准确来说,线程池不能说成是创建线程的方式,而是提供了一个线程队列,避免了额外的创建于开销。这有点向JDBC中的数据库连接池,池子里面已经准备好了一些数据库连接,需要的时候就从池子里面取就行了。
Thread中的API介绍
通过上面的介绍,我们已经知道了什么是线程,如何去创建一个线程,下面就介绍一下Thread类中的几个重要API(关于java.util.concurrent的包中的类与API后续笔记会有说明,但是其实很多方法都是Thread中方法的衍生或补充)进行介绍。
start(): 使当前线程开始执行,Java虚拟机调用该线程的run()方法
sleep(long millis): 在指定的毫秒数内让正在执行的线程休眠(暂停执行)。注意,这是一个静态方法,直接使用 Thread.sleep() 调用,并且该方法会抛出InterruptedException(中断异常)
currentThread(): 放回当前正在执行的线程对象的引用。这是一个静态方法,使用Thread.currentThread()调用。
getId(): 返回该线程的标识符。线程ID是一个正的long值,在创建线程时生成,并且是唯一的,终生不变。在线程终止时,该线程ID可以被重新使用。
getState(): 返回该线程的状态。线程由如下几种状态:
- NEW: 线程实例化后还未执行start()方法时的状态
- RUNNABLE: 线程进入运行状态
- TERMINATED: 线程被销毁时的状态
- BLOCKED: 某一线程等待锁
- WAITING: 线程执行了Object.wait()方法后出现的状态
setName(): 设置当前线程的名字
getName(): 返回当前线程的名字
isAlive(): 测试当前线程是否处于活动状态(如果线程已经启动且尚未终止,则为活动状态)
setPriority(int newPriority): 更改线程的优先级。下面对线程优先级进行说明:
在Java中,线程的优先级分为1~10个等级,如果优先级小于1或大于10,则抛出IllegalArgumentExceptin()异常。在JDK中使用三个常量定义了三个优先级的值:
- public final static int MIN_PRIORITY = 1;
- public final static int NORM_PRIORITY=5;
- public final static int MAX_PRIORITY=10;
- 高优先级的线程总是大部分先执行完,但不代表高优先级的线程都执行完
- 并非先被main线程调用就会先执行完
- 当线程的优先级等级差距很大时,谁先执行与调用顺序无关
注意:不要把线程的优先级与运行结果的顺序作为衡量标准,线程优先级与打印顺序无关,这说明线程的优先级还具有一定的随机性
getPriority(int newPriority): 返回线程的优先级
interrupted(): 测试当前线程是否已经中断(静态方法)。线程的中断状态由该方法清除,如果两次调用该方法,第二次将返回false(假如第一次调用时当前线程处于中断状态,在返回true的同时把中断状态清除,第二次调用时将返回false)
isInterrupted(): 测试当前线程是否已经中断(不是静态方法,不会清除中断状态)
interrupt(): 中断当前线程
yield(): 暂停当前正在执行的线程对象,并执行其他线程(放弃当前的CPU资源,将它让给其他任务去占用CPU时间,但放弃时间不确定)
join(): 等待线程终止。理解:主线程等待子线程终止。main是主线程,在main线程中创建子线程thread,并在main线程中调用thread.join(),那么main线程要等待thread线程执行后再执行。
具体解释:在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时运算,主线程往往将在子线程之前结束,但是如果主线程处理完其他的事物后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这时就需要用到join()方法。join()方法具有使线程排队运行的作用,有些类似于同步的运行效果。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理做为同步
1 | //下面给出的是join()方法的示例 |
this和Thread.currentThread()
刚开始学习线程的时候可能会对this和Thread.currentThread()有点迷,傻傻分不清这两个到底有什么区别,现在我就来详细解释一下这两个的区别和联系,先看一段代码,通过结果来分析。
1 | public class CountOperate extends Thread { |
this是什么: 如果线程类是继承java.lang.Thread,那么线程类就可以使用this关键字去调用继承自父类的Thread方法,this代表当前的线程对象.
Thread.currentThread(): Thread.currentThread()可以获取当前线程的引用,一般是在没有线程对象又需要获得线程信息时可以通过Thread.currentThread()获得当前代码段所在线程的引用。
this和Thread.currentThread()的区别:
在构造器中,this代表的是当前线程对象(通过构造器生成的线程对象),但是由于线程对象正在生产,还没有start,所以线程状态为false。但是在构造器中,Thread.currentThread()表示的是mian线程(main线程是主线程,其实并非所有的线程都是我们手动开启,还有一些线程是JVM自动开启的,比如垃圾回收线程,主线程等),所以线程状态为true。
在run方法中,this和Thread.currentThread()代表的都是当前线程对象的引用。由于线程已经start,所以线程状态就是true
参考书籍
- 《Java多线程编程核心技术》