线程是学习java编程必须要学习的知识模块,其重要程度毋庸赘言。本篇的目的就是尽量全面地介绍一下程相关的基础知识,包括一些概念定义、使用方式、注意事项等。废话少说,现在开始。
一、为什么要使用多线程
大部分人都知道多线程,并知道如何使用多线程,但是使用多线程的原因,一般都不会去管。为了更好地理解多线程,我们有必要去弄清楚这个问题。也就是弄清楚使用多线程的好处是什么。我认为使用多线程的好处大致有两点:
1、提高系统资源利用率
想象一下这个场景:一个程序需要从硬盘中读取文件,然后处理文件。假设读取一个文件需要5秒,处理一个文件需要2秒。读取并处理两个文件,那么在单线程模式下,完成任务所需时间是:5+2+5+2 = 14秒;由于磁盘的IO速度比CPU和内存的IO速度慢很多,所以在磁盘读取文件的时候,CPU大部分时间是空闲的。所以在多线程的情况下,我们可以使用一个线程读取文件,当一个文件读取完后另起一个线程去处理文件,此时文件读取线程继续读物第二个文件。这样文成任务所需时间是:5+5+2 = 12秒。
还有一个更加直观的场景是:一个单线程的程序不可能使得CPU的使用率达到100%,可能只有10%-20%,那么剩下的80%-90%的CPU资源就空闲了下来。此时如果有另外一个线程并发运行,自然就提高了CPU的占用率。
2、提高响应速度
假设这样一个场景:有一个服务器,可以接收多个客户端的请求。如果服务器是单线程,那么只能在处理完一个客户端的请求后,才能响应下一个客户端的请求。如果客户端请求比较耗时,或者请求较多,那么后面的客户端的请求就会长时间得不到响应。如果是多线程的话,可以将客户请求分配给多个线程去执行,自然对后面客户端的请求响应更快。
二、线程与进程的定义
程序:系统要完成一个任务就是一个程序
进程:每个程序都运行在一个单独的进程中。
线程:每个进程可以有多个顺序执行流,每个执行流就可以看做是一个线程。
1、进程的特性
1)独立性:每个进程都是系统中独立存在的实体,每个进程有自己独立的资源和空间地址,一个进程不能访问另外一个进程空间里面的数据。一个进程至少有一个线程。
2)动态性:进程和程序一个是动态的一个是静态的。程序是一组静态指令的合集,而进程是系统正在运行的指令集合。
3)并发性:多个进程可以交替执行,提高程序执行效率和系统资源利用率
2、线程的特性
1)线程是进程中负责执行的单元,依靠程序运行。一个进程里面可以有多个线程。创建一个线程时,系统不会为这个线程重新分配内存,线程使用的是所属进程的内存资源。
2)不同于进程的独立性,线程之间是可以共享内存中的数据的。
3)创建一个线程的代价比创建一个进程的代价小很多,所以用多线程代替多进程实现多任务并发效率会高很多。
三、并发与并行
并发:在一个时间段中有多个程序都处在开始和完成的阶段之间,且这几个程序都在同一个CPU上运行,但在一个时间点上只有一个程序在运行,这就叫做并发。并发的定义是基于时间段而不是时间点的。
并行:当一个系统同有多个CPU时,系统的运行可能并不是并发,而是并行。也就是在同一个时间点上,一个 CPU执行一个线程时,另一个CPU可能执行另一个线程,两个线程时同时执行的,这就叫并行。
并行和并发的区别是:并发是针对时间段来说的,并行是针对时间点来说的。并发的话,一个时间段可以有多个线程运行,但是一个时间点上只能有一个线程执行;而并行的话,在一个时间点上,不同的CPU可以执行不同的线程。
这里还要说一下CPU的使用率是怎样定义的。
CPU使用率:系统会把一个CPU运行时间划分成若干个时间片,把这些时间片分配到不同的程序,让他们交替运行。假设在一个时间段中程序A占用了CPU 30ms的运行时间,程序B占用10ms,然后CPU休息60ms,那么CPU的使用率就是40%。
四、线程的实现
创建一个线程后,我们可以定义这个线程需要执行的任务。也就是要实现Runnable接口的run方法。线程的实现方式分为两种:
1、通过继承Thread类
Thread类已经实现了Runnable接口,并重写了run()方法。这个实现线程的方法要求继承Thread类并重写run()方法,如下:
public class MyThread extends Thread {
override
public void run() {
Log.d("test", "run in MyThread!");
}
}
定义好线程类后,创建一个线程类对象
MyThread thread = new MyThread();
然后调用线程对象的start()方法,才会启动线程。注意,启动线程不是调用线程的run()方法。
thread.start();
2、通过实现Runnable接口
这种方式是我们创建一个子类实现Runnable接口,然后在创建Thread的对象的时候,将Runnable子类的实例作为参数传入。如下:
public class MyRunnable implements Runnable {
override
public void run() {
Log.d("test", "run in MyRunnable!");
}
}
在创建Thread对象的时候传入MyRunnable实例:
Thread thread = new Thread(new MyRunnable());
thread.start();
3、Thread类常用方法
sleep(long millis):这是个静态方法,调用这个方法后,当前线程会交出CPU的执行权限,并进入blocked状态,但这个方法不会释放锁。也就是说如果当前对象持有对某个对象的锁,即使当前线程sleep了,其它线程也不能访问这个对象。通过sleep方法交出CPU执行权限后,低优先级的线程也有机会获得CPU执行权限。
yield():这和sleep()方法一样也是静态方法,调用后也会交出CPU执行权限,并且不会释放锁。只不过和sleep()方法不同的是,调用yield()方法后线程是进入就绪(Runable)状态,只需等待下一次分配CPU执行权限就好。并且通过yield()方法释放CPU执行权限后,低优先级的线程是没有机会获取CPU执行权限的,之后同优先级或者高优先级的线程才有机会。
setDaemon(boolean on):用来设置线程是否为守护线程。需要注意的是,如果要将一个线程设置为守护线程,则必须在线程调用start()方法之前,而不能在线程运行的时候调用setDaemon()方法,否则会抛出异常。
守护线程与用户线程的区别在于,守护线程是依赖于创建它的线程的,而用户线程不依赖。举个例子:如果在main线程中创建了一个守护线程和一个用户线程,当main执行完毕后,守护线程也会随之消亡,而用户线程不会,除非它的run方法执行完毕。
join()和join(long milli):调用join()方法后,要等到调用join()方法的线程执行完毕后,其它线程才能执行。join(long milli)的参数用于指定线程此次最长的执行时间,超过这个时间后,即使线程没有执行完,也会交出CPU执行权限。
其它的方法比较常用,也比较简单,这里就不再细说了。
五、线程状态
1、线程状态定义及相互装换
线程的状态有五个,分别是:New(新建状态)、Runable(就绪状态)、Running(运行状态)、Blocked(阻塞状态)和Dead(死亡状态)
新建状态(New):通过new语句创建的线程,还没有调用start()方法之前的状态
就绪状态(Runnable):New状态的线程调用start()方法后就变为Runable状态。这个状态的线程随时可能被CPU调度执行,也就是获得CPU执行权限。
运行状态(Runable):获得CPU执行权限的线程就处于运行状态。值得注意的是,线程只能从就绪状态进入到运行状态。
阻塞状态(Blocked):阻塞状态是因为线程因为某种原因,放弃CPU使用权而进入的状态。阻塞状态只有先进入到就绪状态之后,才有可能获取CPU执行权限,进入到运行状态。
阻塞的情况分为三种:
1)等待阻塞:当线程调用wait()方法时,线程进入阻塞状态,直到有对象调用notify()或者notifyAll()才能进入到就绪状态
2)同步阻塞:当线程竞争同步锁失败(因为锁被其它线程占用)时,线程进入到阻塞状态。只有当获得同步锁的线程释放了同步锁之后,其它的竞争同步锁的线程才能进入到就绪状态。
3)其它阻塞:当线程调用sleep()、join()或者发出了I/O请求时,导致线程阻塞。当sleep()状态超时、join()等待线程中止或者I/O操作执行完毕时,线程才重新进入到就绪状态。
死亡状态(Dead):当线程的执行完毕(也就是线程的run方法执行完毕)后,或者在执行run()方法的时候因异常退出,线程就进入到死亡状态。
我们看一下线程状态转换图:
线程状态及状态之间的转化就简单介绍到这里。清楚线程的状态及其转化关系对于理解线程同步是非常有帮助的。所以应该对线程的转化关系和转化条件有比较清楚的认识。