1、基本概念
进程是程序的一次动态执行过程,是系统进行资源分配和调度运行的基本单位。
线程是进程的一个实体,它是比进程更小的的能够独立运行的基本单位。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。线程基本自己不拥有系统资源,只拥有一点在运行中必不可少的资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。它可与同属一个进程中的其他线程共享进程所拥有的全部资源。
2、引入线程与进程的目的
在OS中引入进程的目的是为了使更多的程序能够并发执行,以提高资源的利用率和系统的吞吐量。
引入线程的则是为了减少程序在并发执行时所付出的时间开销,使OS具有更好的并发性,以进一步提高资源的利用率和系统的吞吐量。
3、程序、进程与线程的关系
程序是保存在硬盘上的可执行代码,是一种静态的概念。
进程是一个程序在计算机上执行的过程,是一种动态的概念。
线程是进程的一部分,进程包含多个线程在运行。
形象的比喻:将计算机的CPU比作一座工厂,它承担了所有的计算任务。
由于单个CPU一次只能运行一个任务,所以此工厂一次只能供给一个车间。进程好比是这个车间,任何一个时刻CPU总是运行一个进程,其他进程处于非运行状态。
一个车间里有很多工人协同完成一个任务,线程好比车间里的工人,一个进程可以包括多个线程。
车间的空间是工人们共享的,这象征着一个进程的内存空间是共享的,每个其包括的线程可以使用这些共享内存。
可每间房间的大小不同,有些房间只能容纳一个人,比如厕所,里面有人的时候,其他人就不能进入。这代表一个线程使用某些共享内存的时,其他线程必须等它结束,才能使用这一块内存。
一个人防止他人进入的简单方法就是在门口加一把锁。先到的人锁上门,后来的人看见上锁就在门口排队,等锁打开在进入,这叫做“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读取某一块内存区。
具体比如: Word程序的源代码就是保存在硬盘上的可执行代码,是静态的,当启动Word程序时对于操作系统就相当于启动了一个进程,而在这个进程还有许多功能(如拼写检查)是通过一个个线程实现的。如果Word关闭了,则这些拼写检查的线程也将消失,但是如果拼写检查的线程关闭了,也并不会让Word的进程关闭。
4、任何线程一般具有5种状态:创建、就绪、运行、阻塞、终止
创建状态:新建一个线程对象可采用Thread类的构造方法来实现。
就绪状态:调用start()方法启动线程,此时线程进入线程队列排队,等待CPU的服务。
运行状态:调运线程对象的run()方法。
阻塞状态:在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程进入阻塞状态
只有当引起阻塞状态的原因消失时,线程才可以进入就绪状态。
死亡状态:调用stop()方法或run()方法执行结束时。
5、Java中的多线程
1.Java中的实现方式:
在Java中线要实现多线程操作有两种常用的手段: 一种是继承Thread类,另外一种是实现Runable接口。
通过继承Thread类实现多线程必然受到单继承的局限性的影响,并且实现Runable接口适合资源共享,所以一般来说,都是通过实现Runable接口来实现多继承。
两种方式都必须明确覆写run()方法,此方法为线程的主体。
继承Thread类要通过start()方法启动线程,而Runable接口还是要依靠Thread类的start()方法来启动线程。
2.Thread类与Runable接口的联系与区别:
联系:
1.Thread的类也是Runable接口的子类,但是Thread类中并没有完全实现Runable接口中run()方法,所以两种方式都必须明确覆写run()方法。
public class Thread extends Object implements Runnable
2.二者的操作方式类似于代理模式:
Thread类与Runable接口的子类都同时实现了Runable接口,之后将Runable的子类放到Thread类的构造器中,调用Thread类的start()方法启动线程。
区别:
1.启动线程的方式不同
2.局限性不同
3.实现Runable接口的对象可以方便的实现资源的共享,而继承Thread类则多个程序之间无法共享线程类的实例对象。
(原因:当继承Thread类实现多线程时,程序每新建一个线程都需要创建一个Thread类对象,Thread类对象的执行体run()方法是独立的,因此每个因此多个线程之间不能共享实例变量。 而以实现Runable接口的对象作为Thread对象的target,Runable实现类的run()方法仅作为Thread对象的线程执行体,而实际的的线程对象依然是Thread的实例。多个线程可以共享一个target,所以一多个线程共享一个Runable对象的实例变量。)
3.为什么启动线程时不能直接使用run()方法:
调用start()方法来启动线程,系统会把该run()当成线程的执行体来处理;但如果直接调用线程对象的run()方法,则run()将会被当做一个普通方法被执行,而不是线程的执行体。
6、死锁的概念
7、进程同步
在多道程序环境下,系统中各进程以不可预测的速度向前推进,进程的异步性对临界资源的访问会给系统造成混乱,造成了结果的不可再现性。为防止这种现象,引入进程同步的概念。
对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源(例如现实生活中的厕所就是一个典型的临界资源)。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。
解决同步问题的两种方式:
同步代码块
synchronized(同步对象){
...
//需要同步的代码; }
同步方法
synchronized 关键字将一个方法声明为同步方法。
//synchronized的音标['sɪŋkrənaɪzd]
对于临界区的访问过程分为四个部分:
1.进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞
2.临界区:在临界区做操作
3.退出区:清除临界区被占用的标志
4.剩余区:进程与临界区不相关部分的代码
8、线程池
引入原因:
系统启动一个新的线程的成本比较高,因为它涉及与操作系统交互。在这种情况下,使用线程池可以很好的提高性能。尤其是当程序中需要创建大量的生存期很短暂的线程时,更应该考虑使用线程池。
实现过程:
线程池在系统启动时即创建大量空闲的线程,程序将一个Runable对象传给线程池,线程池就会启动一个线程来执行run()方法,当run()方法结束后,该线程并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个Runable对象的run()方法。
其他优势:
线程池还可以有效的控制系统中并发进程的数量,通过设置线程池的最大线程数参数可以控制系统中并发线程数不超过此数,避免系统中包含大量并发线程导致JVM崩溃。