线程和进程
主要区别
根本区别:
- 进程是是系统进行资源分配和调度的基本单位
- 线程是进程中的一个实体,是被系统独立调度和分派的基本单位
在开销方面:
- 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销
- 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
资源和内存方面:
- 进程是系统中一个活动的实体,它可以拥有自己独立的资源。每个进程都拥有自己独立的内存空间。
- 除了CPU外,系统不会为线程分配内存。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源
并发与并行的区别
多个进程在单个处理器上并发执行,同一进程中的多个线程之间可以并发执行。
说到并发,还有一个与之相关的并行。并发与并行是两个既相似而又不相同的概念:
- 并发性,又称共行性,是指能处理多个同时性活动的能力。是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。;
- 并行是指同时发生的两个并发事件,即每个cpu运行一个程序。并行具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。
总的来说,并发指在同一时刻只能有一条指令执行,但多个指令被快速轮换执行,使得在宏观上具有多个活动同时进行的效果。而并行指在同一时刻,有多条指令在多个处理器上同时执行。
包含关系
- 线程是进程的组成部分,一个进程可以拥有多个线程,一个进程必须有一个父进程。
- 如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
- 一个程序运行后至少包含一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
多线程的优势
- 进程之间不能共享内存,但线程之间共享内存非常容易。(因为多个进程共享父进程的内存空间)
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多(同样是因为同一个进程下的线程共享进程的资源),因此使用多线程来实现多任务并发比多进程效率高。
- Java内置了多线程功能支持,从而简化了Java的多线程编程。
创建多线程
继承Thread类创建线程类
1 | public class FirstThread extends Thread { |
1 | public class TestFirstThread { |
在main方法中创建了两次该线程类,并调用start方法开启线程,然后执行线程中的run方法。
注意:使用继承Thread类创建线程类时,不同线程对象不能共享该线程类的成员变量
上述代码输出结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Thread-0: i = 0
Thread-1: i = 0
Thread-1: i = 1
Thread-0: i = 1
Thread-1: i = 2
Thread-1: i = 3
Thread-1: i = 4
Thread-1: i = 5
Thread-1: i = 6
Thread-1: i = 7
Thread-1: i = 8
Thread-1: i = 9
Thread-0: i = 2
Thread-0: i = 3
Thread-0: i = 4
Thread-0: i = 5
Thread-0: i = 6
Thread-0: i = 7
Thread-0: i = 8
Thread-0: i = 9
实现Runnable接口创建线程
1 | public class SecondThread implements Runnable { |
1 | public class TestSecondThread { |
运行结果
1 | Thread-0: i = 0 |
从运行结果可以发现用这种方法创建的多个线程可以共享线程类(严格来说不是线程类,只是和线程类一样实现了Runnable接口,说它是线程类的target更合适)的成员变量。下面会分析为什么会这样。
从源码角度分析以上两种方法
其实查看源码可以发现,以上这两种方法虽然实现不同,但内在的原理是一样的。
继承Thread
1 | class Thread implements Runnable { |
可以看到Thread实现了Runnable接口并重写了run方法。FirstThread方法继承了Thread,其实也就是实现了Runnable接口,然后重写了run方法。
第一种方法开启线程是这样的:new FirstThread().start(); 它执行了Thread的无参构造方法
1 | public Thread() { |
我们重点看第二个参数Runnable target,这里的target为null,也就是说第一种方式实现不需要target,但第二种方式会不一样。
实现Runnable
1 |
|
可以看到Runnable接口只有一个方法,而SecondThread实现了该接口并重写了这个方法。所以实际上SecondThread也是一个Runnable,也就是说SecondThread可以作为Thread类的一个target。
第二种方法开启线程是这样的:new Thread(st).start(); 它执行了Thread的含有一个Runnable参数的构造方法
1 | public Thread(Runnable target) { |
这里的第二个参数不在是null,而是传来的参数target,然后在init方法执行过后,SecondThread对象被赋给了Thread的成员变量1
private Runnable target;
所以现在虽然Thread的run方法没有被重写,但它实际上是调用了target的run方法,也就是SecondThread中重写的run方法。不妨再看一次Thread重写的run方法:
1 |
|
所以说,这两种方法的实质都是重写了Thread中的run方法,只不过是直接重写该方法和调用target所重写的该方法的区别。
两种方式所带来的不同
分析过源码后,现在就更加清楚为什么不同的实现,会产生成员变量是否可以共享的问题。
首先是第一种方式,这里new了两个Thread的子类,也就是说有两个对象,等于实现了两个接口对象,那么接口中的run方法的变量自然不能共享了。
1
2new FirstThread().start();
new FirstThread().start();然后是第二种,这里虽然也是new了两个Thread,但是只有一个接口对象,就是st,那么接口中的run方法的变量自然就可以共享了(因为就只有这一个接口对象,也就是说只有这个变量,当然只能共享了)。
1 | SecondThread st = new SecondThread(); //作为target传入Thread的构造方法 |
其实弄懂了这些后,我们也可以用第二种方式实现第一种的效果,只要多创建一个SecondThread(Runnable)对象就行了。
1 | SecondThread st = new SecondThread(); |
改为上述代码之后,再运行就会和第一种方式的效果一样。原因很简单,这里多了一个SecondThread对象,两者的变量当然是不能共享的,因为根本就不在同一块存储空间。
使用Callable和Future创建线程
Callable接口
Callable接口提供了一个call方法作为线程执行体,call方法比run方法更加强大。
- call方法可以有返回值
- call方法可以声明抛出异常
但是Callable接口不是Runnable接口的子接口,所以不能作为Thread的target。那么要怎么把Callable和Thread联系起来呢?
Java5提供了Future接口来代表Callable接口里call方法的返回值,并提供了一个FutureTask实现类(该类既实现了Future接口,又实现了Runnable接口,所以可以作为Thread的target)
创建线程步骤
- 创建一个Callable接口对象,重写call方法
- FutureTask对象并使用FutureTask来包装Callable对象
- 将FutureTask对象作为target传入Thread的构造函数
- 最后可以通过FutureTask对象的get()方法获得Callable线程的返回值
注意
- FutureTask对象的泛型参数必须和Callable对象的泛型参数一致
- call方法会导致主线程阻塞,直到call方法结束并返回为止
三种方式的对比
实现Runnable接口、Callable接口的方式基本相同,可以归为一类。它们和直接继承Thread类的优缺点如下。
实现Runnable接口、Callable接口的优缺点
优点
直接继承Thread类的优缺点
优点
总结
一般推荐采用实现Runnable接口、Callable接口的方法创建线程