0 为什么使用多线程
实际上,操作系统的多进程实现了多任务并发执行,程序的多线程实现了进程的并发执行。多任务、多进程、多线程的前提都是要求操作系统提供多任务、多进程、多线程的支持。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
那么何时会用到多线程呢?大多情况下,要用到多线程的主要是需要处理大量的IO操作或处理数据需要花大量的时间等,比如:读写文件、遥感数据的接收、处理、显示、保存等。
1 进程和线程
1.1 进程Process
1.1.1 什么是进程
进程是操作系统中对正在运行程序的一个抽象。一个进程是某类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。进程可以分2种类型:
1)前台进程:同用户交互并且替他们完成工作的那些进程。
2)后天进程(守护进程daemon):与特定的用户没用关系,具有某些专门功能的进程,如邮件后台定时检查是否有新邮件的进程。
1.1.2 进程状态
进程有三种状态:运行、阻塞、就绪。
1)运行:该时刻进程实际占用COU。当一个进程在处理机上运行时,则称该进程处于运行状态。
2)阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
3)就绪:可运行,但因为其他进程正在运行而暂时停止。当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
对于一个实际的系统,进程的状态及其转换更为复杂。引入新建态和终止态构成了进程的五态模型。
1.1.3 进程间通信的方法
TODO进程和线程通信需要解决的3个问题P66
1.管道(pipe)及有名管道(named pipe):
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2.信号(signal):
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
3.消息队列(message queue):
消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
4.共享内存(shared memory):
可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5.信号量(semaphore):
主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6.套接字(socket);
这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
1.3.4 进程间调度
关于进程和线程的调度算法见:几种常用调度算法
1.2 线程 Thread
1.2.1 什么是线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程共享进程的资源,以便多个线程可以完成某一任务而共同工作。
线程拥有自己的寄存器(用来保存线程当前的工作变量),堆栈(用来记录执行历史)。引入线程主要有如下好处:
1)易于调度。线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
2)开销少。创建线程比创建进程要快,所需开销很少。
3)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
4)利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
Java中常用的几个线程术语:
1)主线程:JVM调用程序mian()所产生的线程。
2)当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
3)后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。
4) 前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
1.2.2 线程状态
线程和进程一样也有3种状态,其状态转换图与进程状态转换图一样,如下(completelife cycle of a thread):
1、运行:该时刻进程实际占用CPU。
2、阻塞:也称为等待或睡眠状态,阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
3、就绪:该状态的线程位于可运行线程池中,变得可运行,等待获取CPU使用权。
下面介绍下JAVA中线程Thread类的几个常用方法,可以与上面的线程状态对应起来。
1)线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪状态。sleep()平台移植性好。
2)线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的notify() 方法或notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用wait(0) 一样。
3)线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
4)线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
5)线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线 程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个wait 方法,在对象的监视器上等待。直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线 程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上 等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
1.2.3 线程间通信
进程的通信解决方法同样适用于线程间的通信。
多线程间共享数据的时候要解决好同步的问题(TODO后期有时间写一篇介绍JAVA同步和异步编程的博客)
1.2.4 线程间调度
线程调度是指按照特定的机制为多个线程分配CPU的使用权。调度的模式有两种:分时调度和抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。在Java程序中,JVM负责线程的调度。JVM的线程调度模式采用了抢占式模式。
关于进程和线程的调度算法见:几种常用调度算法 。这里详细说下JAVA的线程优先级
Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10。Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级,Thread类有以下三个静态常量:
1)staticint MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
2)staticint MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
3)staticint NORM_PRIORITY
分配给线程的默认优先级,取值为5。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有的三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式
2、进程和线程的联系和区别
1)进程只有一个父进程,但可以有多个子线程。
TODO父线程,子线程关系呢?
2)一个程序至少有一个进程,一个进程至少有一个线程。
3)每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
3)一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
4)资源
进程切换的代价比线程高。对于一个进程中的多个线程来说,多个线程共享进程的内存块(地址空间),当有新的线程产生的时候,操作系统不分配新的内存,而是让新线程共享原有的进程块的内存。因此,线程间的通信很容易,速度也很快。不同的进程因为处于不同的内存块,因此进程之间的通信相对困难。
5)安全性
进程之间有保护,线程之间无保护。一个进程中的多个线程有完全一样的地址空间(即所属进程的地址空间),因此可共享同样的全局变量。由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读、写或清除另一个线程的堆栈。线程之间是没有保护的。
3 如何衡量并行效果-加速比
并行计算中的加速比是用并行前的执行速度和并行后的执行速度之比来表示的,它表示了在并行化之后的效率提升情况。阿姆达尔定律(英语:Amdahl's law,Amdahl's argument),一个计算机科学界的经验法则,因吉恩·阿姆达尔而得名,它代表了处理器平行运算之后效率提升的能力。阿姆达尔定律是固定负载(计算总量不变时)时的量化标准,其公式如下:
S=1/ (F+ ((1-F)/n))。
其中,F为串行计算部分所占比例(1-F为并行计算部分所占比例),n为处理器个数。这样,当F=0时,最大加速比s=n;当F=1时,最小加速比s=1;当n→∞时,极限加速比s→ 1/F,这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。
4 应用实例
多线程和TCP协议编程(TODO 有时间写一篇关于利用TCP协议和多线程编程的例子)
例子:多线程:输入线程(把数据读入缓冲区),处理线程(处理数据),输出线程(写出数据)
5 相关笔试题
【记不清了】以下哪些进程状态转换是不正确的()
A.就绪到运行 B.运行到就绪 C.运行到阻塞 D.阻塞到运行 E.阻塞到就绪
选D:根据进程状态图,阻塞需要先到就绪状态,然后从就绪状态到运行状态,而不能直接从阻塞状态到运行状态。
【2013年阿里巴巴暑期实习招聘笔试题】下面不能用作linux进程间通信的是_(选D)
A.共享内存 B.管道名 C.信号量 D.临界区
【Google 2012校招笔试】下面哪种机制可以用来进行进程间通信?
A.Socket B.PIPE C.SHARED MEMORY D.以上皆可
分析:选D。
【微软2013暑期实习笔试题】About the Thread and Process in Windows, which description(s) is(are) correct:(3 Points)
A. One application in OS must have one Process, but not a necessary to have one Thread
B. The Process could have its own Stack but the thread only could share the Stack of its parent Process
C. Thread must belongs to a Process
D. Thread could change its belonging Process
分析:选C(见博客进程和线程的联系和区别)
6 相关学习书籍推荐
1)《JAVA编程思想》:学习多线程编程的基础知识
2)《Java Concurrency in Practice》,中文名为《Java并发编程实战》:详细了解多线程编程的各个方面,和需要注意的事项。电子版本点击这里(中文版本)可下载。