线程和进程的基本概念
一、进程和线程
进程
进程:指在系统中正在运行的一个应用程序。
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间。
比如同时打开QQ、Xcode,系统就会分别启动2个进程
通过“活动监视器”可以查看Mac系统中所开启的进程
线程
线程:一个进程想要执行任务,必须得有线程(每个进程至少有一个线程)
线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行
比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行
线程的串行
一个线程中任务的执行是串行的。如果要在一个线程中执行多个任务,那么只能一个一个按照顺序去执行。在同一时间里,一个线程只能执行一个任务。
在一个线程中下载3个文件(分别是A、B、C)
二、多线程
多线程:一个进程中可以开启多个线程。每条线城可以并行(同时)执行不同的任务。
进程 -> 车间,线城 -> 车间工人
同时开启3条线程分别下载3个文件(A、B、C)
多线程原理
同一时间,CPU只能处理一条线程,只有一条线程在工作(执行),多线程并发(同时)进行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发访问的假象。
思考:如果线程非常多,会发生什么情况?
CPU在N多线程之间调度,CPU会累死,消耗大量CPU资源。每条线程被调度执行的频次会降低(线程执行的效率降低)
多线程的优点
- 能适当提高程序执行效率
- 能适当提高资源利用率(CPU、内存利用率)
多线程的缺点
- 开启线程需要占用一定的内存空间(默认情况下下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量内存空间,降低程序性能。
- 线程越多,CPU在调度线程上的开销就越大
- 程序设计更加复杂:比如线程之间的通信,多线程的数据共享
并发与并行
并行:多个CPU实例或多台机器同时执行一段处理逻辑,是真正的同时
并发:通过CPU调度算法,让用户看上去同时执行,实际上从CPU操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源网网产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
线程安全:经常用来描述一段代码。指在并发情况下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只关注系统的内存,CPU是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
如不加事务的转账代码
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
同步:Java中的同步指的是人为控制和调度,保证共享资源的多线程访问完成线程安全,来保证结果的准确。如上面的代码简单加上@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
三、线程的状态
新建状态(NEW):新创建一个线程对象。
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start() 方法,该状态的线程位于可运行线程池中,等待获取CPU的使用权。
运行状态(Running):就绪状态的线程获取了CPU,执行代码程序。
-
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞分三种:
- 等待阻塞:运行到线程执行了 wait() 方法,JVM会把该线程放入等待池中。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别到线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行到线程执行sleep()或join()方法,或者发出I/O请求时,JVM会把线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因为异常退出了run()方法,该线程结束生命周期。
四、创建线程的三种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable和Future
继承Thread类
- 定义Thread类的子类,并重写run方法,该方法的方法体是需要线程完成的任务
- 创建Thread子类的实例,也就创建了线程的对象
- 启动线程,即是调用线程的start()方法。
注意:线程的start()和run()方法都可以被线程实例调用,只有调用start()方法才是开启线程,拥有线程的特性,而只调用run()方法,相当于普通方法。
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
实现Runnable接口
- 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
- 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
- 第三部依然是通过调用线程对象的start()方法来启动线程
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
使用Callable和Future
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
- call()方法可以有返回值
- call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
- boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
- V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
- V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
- boolean isDone():若Callable任务完成,返回True
- boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
创建病启动有返回值的线程
- 创建Callable接口的实现类,并实现call()方法,然后创建该类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class Main {
public static void main(String[] args){
MyThread3 th=new MyThread3();
//使用Lambda表达式创建Callable对象
//使用FutureTask类来包装Callable对象
FutureTask<Integer> future=new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
三种创建线程方式的对比
实现Runnable和实现Callable接口的方式基本相同,不过Callable执行call()方法有返回值,Runnable执行run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
注:一般推荐采用实现接口的方式来创建多线程
参考: