java进程和线程在面试中几乎是一个必问的基础点。下面我们从以下几个方面来理解进程和线程。
一.并发与并行的概念与区别
二.进程
三.线程的概念以及与进程的区别
四.线程实现方式
五.Thread与Runnable源码分析
一.并发与并行的概念与区别
1.概念
并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
2.区别
从时间的角度看,
并发是在一段时间内多个程序在执行,描述的是多个程序交互执行的过程。
并行是在同一时刻多个程序在执行,描述的是多个程序在多个cpu上同时进行的过程。
二.进程
1.概念
进程:是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。反映了一个程序在一定的数据 集上运行的全部动态过程。通过进程控制块(PCB)唯一的标识某个进程。同时进程占据着相应的资源(例如包
括cpu的使用 ,轮转时间以及一些其它设备的权限)。是系统进行资源分配和调度的一个独立单位。
2.进程与程序的区别
进程是动态的,程序是静态的代码集合。进程具有生命周期,程序没有生命周期。
3.进程的状态
1)就绪(Ready)状态
当进程已分配到除CPU以外的所有必要资源后,只要在获得CPU,便可立即执行,
进程这时的状态就称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,
通常将他们排成一个队列,称为就绪队列。
2)执行状态
进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;
再多处理机系统中,则有多个进程处于执行状态。
3)阻塞状态
正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,
亦即程序的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。
进程状态转换图
三.线程的概念以及与进程的区别
1.线程的概念
是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。 在一个程序中,这些独立运行的程序片段叫作“线程,利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
2.线程的特点
在多线程OS中,通常是在一个进程中包括多个线程,Java中的线程有四种状态分别是:运行、就绪、挂起、结束。每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
3)可并发执行
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
4)共享进程资源
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
3.线程与进程的区别
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
4)在多线程OS中,进程不是一个可执行的实体。即在多线程操作系统中,线程是最小的资源调度的单位,进程仍然是资源分配的单位。
四.线程实现方式
1.继承Thread
通过继承并覆写其中的run()即可,通过start()方法来启动线程
class MyThread extends Thread{
private String name;
public MyThread(String name) {
super();
this.name = name;
}
public void run(){
System.out.println("the name is:"+name);
}
public static void main(String[]args){
MyThread t=new MyThread("xqhadoop");
//通过start方法启动线程
t.start();
}
}
2.实现Runnable接口
由于java中不允许多继承,但可以实现多个接口。所以在实际开发中很少使用Thread,而是通过实现Runnable接口来避免单继承的局限。
class MyThread implements Runnable{
private String name="xqhadoop";
public void run(){
System.out.println("the name is:"+name);
}
}
public static void main(String []args){
//如何创建Runnable线程
MyThread t=new MyThread();
new Thread(t).start();
}
}
在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
1)避免单继承的局限,一个类可以同时实现多个接口。
2) 便于资源的共享,多线程并发。
注:这里为什么为提到资源共享,因为Thread创建都需要Runnable对象。所以可以将Runnable对象传入多个Thread中以达到共享的目的。这里注意不要忘了同步的问题。详细见链接
3、实现Callable接口
Callable接口(也只有一个方法)定义如下:
public interface Callable<V> {
V call() throws Exception; }
实现Callable接口通过FutureTask包装器来创建Thread线程。
public class MyCallable<V> implements Callable<V> {
@Override
public V call() throws Exception {
// TODO Auto-generated method stub
return null;
}
public static void main(String[]args){
Callable oneCallable = new MyCallable();
FutureTask oneTask = new FutureTask<V>(oneCallable);
/*
FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。
*/
Thread oneThread = new Thread(oneTask);
oneThread.start();
}
}
4.使用Executor框架创建并发线程池,
关于多线程部分都在java.util.concurrent里。
Executors类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
创建线程池的基本方法如下:
public static ExecutorService
//创建固定数目线程的线程池。
newFixedThreadPool(int nThreads)
//创建一个可缓存可重用的线程池
public static ExecutorService newCachedThreadPool()
//创建一个单线程化的Executor。
public static ExecutorService newSingleThreadExecutor()
//创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
1)无返回值的线程池
无返回值的任务类必须实现Runnable接口
/**
* 无返回值的线程
*/
public class ThreadTest{
//创建Runnable线程接口实现类
class MyRunnable implements Runnable{
public void run(){
System.out.println("this is runnable...");
}
}
//主方法
public static void main(String[]args){
ExecutorService pool=null;
//创建并发的缓存线程
pool=PoolExecutors.newCachedThreadPool()
mThreadPool.execute();
pool.execute(new MyRunnable());
}
}
2)有返回值的线程池
有返回值的任务类需要实现Callable接口
import java.util.concurrent.*;
import java.util.*;
/**
* 有返回值的线程
*/
public class ThreadTest {
public static void main(String[] args) throws Exception {
System.out.println("----程序开始运行----");
// 创建一个线程池
ExecutorServicepool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象(存放执行结果)
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(f.get().toString());
}
}
/**
创建有返回值的任务类
*/
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
return ">>>" + taskNum + "任务完成"
}
}
}
五.Thread与Runnable源码分析
1.Thread与Runnable关系
我们通过源码知道Thread也是实现Runnable接口
public class Thread implements Runnable{
}
2.构造函数
那么这里为什么这两者调用线程执行的方式不同呢?
下面我们从构造函数入手
//Thread默认构造
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
//runnable构造
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//最全的构造方式
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
从这里我们知道无论Thread还是Runnable,初始化都调用了init()函数。
//init()方法来初始化线程
private void init(ThreadGroup g, Runnable target, String name,long stackSize)
唯一不同的是传入的target变量不同,所以解开问题的关键在于target变量,这里的target变量是Runnable的引用。
3.start()启动方法
start()方法中主要是调用了如下方法,
private native void start0();
这里使用了本地调用,通过C代码初始化线程需要的系统资源。可见,线程底层的实现是通过C代码去完成的。start()方法只是将线程的状态变成就绪状态,并未真正执行,等待分配CPU资源。一旦CPU资源分配则立刻执行run()方法。
4.run()方法
public void run() {
if (target != null) {
target.run();
}
}
这里的target实际上要保存的是一个Runnable接口的实现的引用:
private Runnable target;
到这里我们真正的完全理解了Thread和Runnable是如何工作的。
其实两者都是通过创建线程的方式来启动线程的,只是线程初始化不同而已。这里默认都是执行target中的run方法
1)对于继承Thread实现线程来说,默认的构造函数中target变量是未赋值的,所以默认不会执行任何代码。所以需要覆写上述默认的run()方法来实现。
2)对于实现Runnable来达到启动线程来说,通过将Runnable引用传入线程构造中,这里线程会执行target中的run方法。
5.理解测试
如果你能将以下两段测试代码理解,说明你理解了上述源码分析。
1)如下代码段 1:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Run of Runnable");
}
}) {
public void run() {
System.out.println("Run of Thread");
}
}.start();
执行上述代码段,到底会调用哪个run()方法呢?
2) 再看如下代码段 2:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Run of Runnable");
}
}) {
public void run() {
System.out.println("Run of Thread");
super.run();
}
}.start();
执行上述代码段,又会调用哪个run()方法呢?