一、概念
1.1 基本概念
进程是程序执行的一个实例,比如说,10个用户同时执行IE,那么就有10个独立的进程(尽管他们共享同一个可执行代码)。
进程的特点,每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。怎么看待多进程?进程的优点是提高CPU运行效率,在同一时间内执行多个程序,即并发执行。但是从严格上讲,也不是绝对的同一时刻执行多个程序,只不过CPU在执行时通过时间片等调度算法不同进程高速切换。总结来说:
- 进程由操作系统调度,简单而且稳定
- 进程之间的隔离性好,一个进程崩溃不会影响其它进程
- 单进程编程简单
- 在多核情况下可以把进程和CPU进行绑定,充分利用CPU
当然,多进程也有一些缺点:
- 一般来说进程消耗的内存比较大
- 进程切换代价很高,进程切换也像线程一样需要保持上一个进程的上下文环境
- 在web编程中,如果一个进程来处理一个请求的话,如果要提高并发量就要提高进程数,而进程数量受内存和切换代价限制
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
同类的多个线程共享一块内存空间和一组系统资源,线程本身的数据通常只有CPU的寄存器数据,以及一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。
在JVM中,本地方法栈、虚拟机栈和程序计数器是线程隔离的,而堆区和方法区是线程共享的。关于JVM中的资源分配,可参考我的另一篇文章【JVM内存管理及GC】:http://blog.csdn.net/suifeng3051/article/details/48292193
1.2 进程线程的区别
- 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
- 线程是处理器调度的基本单位,但进程不是
- 二者均可并发执行
注: 关于并发与并行
并发:多个事件在同一时间段内一起执行
并行:多个事件在同一时刻同时执行
1.3 多任务
在一开始,一个计算机只有一个CPU,这个CPU一次也只能运行一个任务。然而随着计算机技术的发展,一个CPU也可以“同时”运行多个任务,这就诞生了多任务。但这里的同时并不是真正的同时,操作系统通过切换各个应用来实现CPU的共享,在CPU内部各个程序其实是交替执行的。
1.4 多线程
为了进一步提高CPU利用率,多线程便诞生了。一个程序中可以运行多个线程,多个线程可以同时执行,从整个应用角度上看,这个应用好像独自拥有多个CPU一样。虽然多线程进一步提高了应用的执行效率,但是由于线程之间会共享内存资源,这也会导致一些资源同步问题,另外,线程之间的切换也会对资源有所消耗(后面会讲到)。
这里需要注意的是,如果一台电脑只有一个CPU核心,那么多线程也并没有真正的“同时”运行,它们之间需要通过相互切换来共享CPU核心,所以,只有一个CPU核心的情况下,多线程不会提高应用效率。但是,现代计算机一般都会有多个CPU,并且每个CPU可能还会有多个核心,所以在现代硬件资源条件下,多线程编程可以极大的提高应用效率。
1.5 多线程的调度
在Java程序中,JVM负责线程的调度。线程调度是值按照特定的机制为多个线程分配CPU的使用权。
调度的模式有两种:分时调度和抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。
1.6 多线程编程面临的问题
- 更复杂的设计 : 多线程在访问共享数据时需要进行同步(在java中需要使用synchronized关键字),某些情况下需要考虑线程的执行顺序和相互配合
- 上下文切换: 上CPU需要从一个线程切换到另一个线程时,它需要先保存当前线程的本地数据和程序指针,然后再加载要切换线程的本地数据和程序指针
- 更多的系统资源:处理需要CPU时间以外,每个线程还需要额外的内存空间来保存它的本地数据栈,更需要操作系统资源来管理多个线程,所以应用程序的线程数量一定要根据实际情况合理安排
关于多线程编程中的资源同步,请参考另一篇文章【 Java synchronized 介绍】:http://blog.csdn.net/suifeng3051/article/details/48711405
二、线程的实现
Java中实现多线程,一种是继承Thread类,一种是实现Runable接口。
2.1 继承Thread类
/**
* 继承Thread类,直接调用run方法
* */
class hello extends Thread {
public hello() {
}
public hello(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 " + i);
}
}
public static void main(String[] args) {
hello h1=new hello("A");
hello h2=new hello("B");
h1.start();
h2.start();
}
private String name;
}
注意:在实际启动进程的时候,我们直接调用的并不是Thread子类中run方法,而是调用的Thread线程的start方法,因为线程start运行需要本地操作系统支持,start启动线程会调用操作系统native函数来支持线程运行。
2.2 实现runnable接口
package com.heaven.xiancheng;
public class TestRunnable implements Runnable{
private int count =100;
public void run(){
for(int i=0;i<200;i++){
if(count >0){
System. out.println(Thread.currentThread().getName()+ " "+count --);
}
}
}
public static void main(String[] args) {
TestRunnable r= new TestRunnable();
Thread t1= new Thread(r,"A" );
Thread t2= new Thread(r,"B" );
t1.start();
t2.start();
}
}
2.3 两者区别
实现Runnable接口比继承Thread类有更多的优势,所以我推荐大家尽量使用实现runnable接口的形式,以下是其优点
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
三、线程的状态
3.1 线程的五种状态类型
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
其中阻塞又可能是由以下几种情况造成:
- 调用 sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
- 用 suspend()暂停了线程的执行。除非线程收到 resume()消息,否则不会返回“可运行”状态。
- 用 wait()暂停了线程的执行。除非线程收到 nofify()或者 notifyAll()消息,否则不会变成“可运行“。
- 线程正在等候一些 IO(输入输出)操作完成。
- 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。
3.2 线程状态图
四、线程的阻塞
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
4.1 sleep() 方法
sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
4.2 suspend() 和 resume() 方法
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
4.3 yield() 方法
yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
4.4 wait() 和 notify() 方法
两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。
在这里需要重点介绍下wait()和notify()
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
最后,关于 wait() 和 notify() 方法再说明两点:
1. 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题
2. 除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
这篇文章中有wait()/notify()的代码实例。
五、线程的其它问题
5.1 Thread.Join
把指定的线程加入到当前线程,原本两个线程可以并发执行,join之后变成了两个线程顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new JoinA(),"A");
Thread t2 = new Thread(new JoinB(),"B");
t1.start(); //main函数所在的主线程调用了实现了run()方法的JoinA子线程
t1.join(); //主线程获得子线程的锁,阻塞直到子线程完成
t2.start();
}
}
class JoinA implements Runnable {
private int i;
@Override
public void run() {
while (i <= 10) {
System.out.println(Thread.currentThread().getName() + i + " ");
i++;
}
}
}
class JoinB implements Runnable {
private int i;
@Override
public void run() {
while (i <= 10) {
System.out.println(Thread.currentThread().getName() + i + " ");
i++;
}
}
}
执行上面程序从运行结果可以看出两个线程是顺序执行的。其实是当主线程调用子线程的join()方法时,主线程变获得了子线程对象的锁,因此被子线程阻塞直到子线程退出。
我们可以看一下join()的源码:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join方法实现是通过wait。当main线程调用t.join()时候,main线程会获得线程对象t的锁,调用该对象的wait(),直到该对象唤醒main线程,比如退出后。
5.2 线程的休眠与中断
public class TestInterrupt implements Runnable{
@Override
public void run() {
System. out.println("thread run..." );
try {
System. out.println("begin to sleep..." );
Thread. sleep(10000);
} catch (InterruptedException e) {
System. out.println("sleep was interrupted" );
e.printStackTrace();
}
}
public static void main(String[] args) {
TestInterrupt ti= new TestInterrupt();
Thread t= new Thread(ti);
t.start();
try {
Thread. sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t.interrupt(); //中断线程运行
}
}
5.3 线程的优先级
public class TestPriority implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System. out.println(Thread.currentThread().getName() + "运行" + i);
}
}
public static void main(String[] args) {
TestPriority tp= new TestPriority();
Thread t1= new Thread(tp,"A" );
Thread t2= new Thread(tp,"B" );
Thread t3= new Thread(tp,"C" );
t1.setPriority(1);
t2.setPriority(8);
t3.setPriority(3);
t1.start();
t2.start();
t3.start();
}
}
注意:不要误以为优先级越高就先执行,谁先执行还是取决于谁先取得CPU资源。
5.4 线程的礼让
在线程操作中,也可以使用yield()方法,将一个线程的操作暂时交给其他线程执行。
public class TestYield implements Runnable{
@Override
public void run() {
for(int i=0;i<10;++i){
System. out.println(Thread.currentThread().getName()+ "运行"+i);
if(i==3){
System. out.println("线程的礼让" );
Thread. yield();
}
}
}
public static void main(String[] args) {
Thread h1= new Thread(new TestYield(),"A");
Thread h2= new Thread(new TestYield(),"B");
h1.start();
h2.start();
}
}
5.5 同步与死锁
线程同步问题,当各个线程共用一个资源时,有可能导致线程同步问题。在JAVA中,是没有类似于PV操作、进程互斥等相关的方法的。JAVA的进程同步是通过synchronized()来实现的,需要说明的是,JAVA的synchronized()方法类似于操作系统概念中的互斥内存块,在JAVA中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA中简单的同步、互斥操作。关于这部分内容,请参考我的另一篇文章:
【Javasynchronized介绍】:http://blog.csdn.net/suifeng3051/article/details/48711405
参考文章:
http://blog.csdn.net/bzwm/article/details/3881392
http://www.cnblogs.com/techyc/p/3286678.html