什么是进程?什么是线程?什么是多线程?
想要了解什么是多线程,就得知道什么是线程。
想要了解什么是线程,就得知道什么是进程。
想要了解什么是进程,就得知道什么是一个程序。
那么我们先从程序慢慢说起。
程序
程序表现为系统中的一个可执行文件(该可执行文件为静态)。
说明:双击打开某个文件,不意味着这个文件就是程序
进程
程序(静态的可执行文件)运行起来之后,就是一个进程(运行态的程序)。
或者
对于操作系统来说,一个任务就是一个进程。
补充:
进程是作为系统分配资源的最小单位(CPU,内存等等)
一个进程,系统是分配有唯一的一个id标识(pid)
关于操作系统:需要了解的与进程相关的操作系统知识
1.并发与并行
并发:在系统调度一个CPU,采取时间片轮转调度的方式,在一个时间范围内,执行多个进程的代码。
对于人的肉眼来说,这个时间是不可感知的,就认为是同时在执行。
同一个时间点,只有一个进程在一个CPU执行,但是在一个时间范围,是一个CPU执行多个进程(也就是假同时)
从我们的角度看,是同时执行;
从CPU的角度看,是一个时间点,执行一个进程。
并行:多个CPU在同一个时间点,同时执行多个进程的代码(真同时)。
2.进程运行涉及的:
(1)系统加载进程的程序代码到内存
(2)系统调度CPU执行程序代码指令
3.内核态和用户态
执行的部分就涉及权限问题:
(1)系统操作权限最高,如果一个程序执行某个指令,没有权限,就需要调用系统接口来执行,此时,用户进程就进入“内核态”或者“核心态”。
(2)用户进程来说,权限是最低的,称为“用户态”。
以上两种状态就涉及到互相转换的问题:进程进行权限高的指令(如IO操作),用户态---->内核态,执行完毕,返回;内核态---->用户态。
用户态转变为内核态是比较消耗资源的。(需要注意和考虑的问题点:多线程代码的使用或设计原则,和tcp协议设计原则类似,保证数据安全的情况下,尽可能的提高效率)
4.上下文
简单来说,上下文就是一个环境,进程在时间片轮转切换时,由于每个进程运行环境不同,就涉及到转换前后的上下文环境的切换。
这个名词不是进程和线程独有,而是在环境不同,要进行切换的时候,都需要的词汇。
CPU调度执行进程1,切换到进程2,又切换到进程1(进程1切换到进程2,环境变化就有上文和下文)。
在环境切换出进程1时,保存进程1上文环境,切换到进程2,再切换回进程1时,需要恢复为进程1的上文环境。
认识上下文环境的作用:
- 切换出去时,保存上下文环境
- 切换进来时,恢复上下文环境
5.进程状态
- 就绪:进程处于可运行的状态,只是CPU时间片还没有轮转到该进程,则该进程处于就绪状态。
- 运行:进程处于可运行的状态,且CPU时间片轮转到该进程,该进程正在执行代码,则该进程处于运行状态。
- 阻塞:进程不具备运行条件,正在等待某个时间的完成。
线程(Thread)
1.线程与进程的关系
进程包含多个线程
每个进程至少有一个线程存在,即主线程。
例如:
我是一个进程,要完成写代码、测试APP性能、宣传产品三件事情,那我就依次执行完成这三件事;
如果采取多线程的方式,那就是我雇佣三个人,分别完成以上三件事情。
2.线程和进程的区别(部分):
(1)进程是系统分配资源(CPU,内存)的最小单位,线程是系统调度CPU执行的最小单位。
(2)数据共享:同一个进程内的线程之间可以共享资源,代价比较小;不同进程之间,要共享数据,需要进行进程通信。
多线程
1.多线程作用
提高效率(尽可能充分利用系统资源:CPU)。
需要注意的事项:创建进程是比较耗费时间和资源的,所以要综合考虑同时执行指令(任务量)+创建线程数量+系统可用资源(内存,CPU)。
2.多线程使用场景
一个时间点,要同时做很多件事情(同时执行多行代码)。
例如:
一个QQ打开以后,可以:
(1)收发消息
(2)上传下载文件
(3)查看图片/pdf文件/视频
(以上由多线程完成,否则代码在阻塞等待时无法做其他事情)
又例如:
本地播放视频:
(1)图片:很多图片每秒多少张切换出来,展示出来,就是视频(多少帧)
(2)声音
(3)弹幕,字母(文本内容)
(多线程的方式就是把以上内容,按照时间范围,规定好某个时间点要展示的图片、播放的声音、展示的文本)
3.创建线程的方法
- 继承Thread类:
new一个Thread的子类,Thread就是java中的线程。
Public class MyThread extends Thread{
@Override
public void run(){
System.out.println("线程运行的代码");
}
}
MyThread t = new MyThread();
t.start(); //线程开始运行
Thread的常见构造方法:
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(Runnable target,String name) 使用Runnable对象创建线程对象,并命名
- 实现Runnable接口
new一个Runnable接口的子类,传到Thread中执行。
Runnable是java中任务的概念,new一个Runnable就是定义一个任务的描述,再传入Thread线程中执行。
该方法的好处是可以规避类的单继承的限制,但是需要通过Thread.currentThread()来获取当前线程的引用。
Public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("Thread.currentThread().getName() + 线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
t.start(); //线程开始运行
- 通过Callable创建线程
4.启动线程
start:线程启动的方法(启动以后,才能表现并发,并行的特性)。
申请系统调度线程,让CPU执行(创建态—>就绪态),如果线程获取CPU时间片,就开始执行任务(Tread类中run方法就是任务的定义,或者是Runnable对象中的run方法)
start和run的区别:
start会真正的启动一个线程,以及调用start方法所在的线程,是并发并行执行;
run方法只是一个线程的任务定义,如果直接调用run方法,属于普通方法调用。
5.中断线程
- 使用标志位
可以实现某种程度上的,在满足条件(中断线程的条件:如转账发现诈骗)的情况下,中断一个线程。
存在的问题:如果线程中处于阻塞状态(需要满足一定条件如sleep休眠一定时间,才能恢复),就无法快速的中断线程。
- 基于Thread类本身的api来实现中断
Thread类中,保存有一个中断标志位,初始值=false(没有被中断)
①某个线程的引用.interrupt()
②线程可以获取中断标志位,通过循环判断,来决定是否需要中断
中断某个线程:
(1)线程中断标志位=true
(2)线程要不要中断,线程定义任务的代码自行决定
(3)如果线程处于阻塞状态(调用多线程api方法,显示抛出InterruptrdException的方法)。如果提前让线程从阻塞态转变为就绪态,在系统调度执行后,以抛异常的方式继续执行。(此时线程的中断标志位会重置为false)
6.线程等待
某些情况下,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
- 线程引用对象.join()
- 线程引用对象.join(long)
public class Thread {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
当前线程等待,直到满足以下条件:
(1)无参的方法,就是线程执行完毕
(2)有参的方法,是线程执行完毕和时间到达任意一个满足,满足以后,当前线程继续往下执行
7.线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则收这个程序是线程安全的。
线程不安全的原因:
(1)从代码层面看:多个线程对同一个变量的操作(读,写),有一个写操作,就有线程安全问题。
(2)从原理方面看:
①原子性:多行代码执行,执行时,是一组不可再分的最小执行单位
多个线程同时并发并行的执行代码指令,可能在一个线程操作一个共享变量时,是有前后依赖关系,指令之间有其他线程的操作,就会导致线程不安全。
②可见性:
主存:线程都使用的共享区域,对其中变量/对象的操作
工作内存:线程之间互相不可见,CPU执行线程中的代码指令,是从主存复制到CPU高速缓存
③有序性:
代码重排序:java代码顺序是固定的,但是jvm执行字节码或CPU执行机器码,都可能重排序指令顺序,目的是提高运行效率;但是在多线程中,指令重排序了,代码就会是错误的。
8.如何解决线程不安全的问题
一组代码,如果存在多线程对共享变量的操作,都需要考虑线程安全问题。
一般把多线程操作的共享变量,称为临界资源
一组代码,称为临界区
解决思路:
把临界区加锁,多个线程执行临界区代码,最终表现就是一个线程申请加锁,执行代码,释放锁,其他线程申请失败,需要等待(不一定是阻塞态,也可以是运行态)。
一个线程一个线程依次的执行临界区代码。
线程执行临界区代码---->申请加锁(临界区代码)---->执行临界区代码(申请到锁)---->释放锁---->通知等待的线程再次去申请锁---->自己等待---->下一轮的申请加锁
线程执行临界区代码---->申请加锁(临界区代码)---->等待(没有申请到锁)---->下一轮的申请加锁
- synchronized关键字
同步的关键字:一次执行某段代码
作用:对对象头加锁的方式,保证线程安全,多线程申请同一把锁,会产生同步互斥的作用(多个线程依次执行临界区代码)。
使用:
①在静态方法上:加锁整个方法,锁对象为当前的类对象。
②在实例方法上:加锁整个方法,锁对象为this。
③同步代码块:
synchronized(锁对象){
注意:如果不是使用同一个对象加锁,意味着能同时执行(并发并行)。 - volatile关键字
是修饰一个变量的关键字。
作用:
(1)保证变量的可见性(分解为字节码指令后,有变量的指令,变量有可见性)。
(2)建立一个内存屏障,禁止指令重排序。
使用场景:
(1)多线程对共享变量的操作,如果代码行本身保证了原子性,就可以不加锁,只使用volatile保证可见性,该共享变量的操作也是线程安全的。
(读是原子性,写(修改,赋值)操作;值不依赖共享变量,比如是一个常量,就是原子性)
(2)多线程代码的设计目标:线程安全的情况下,尽可能的提高效率。
9.线程通信
线程并发并行的执行,但是在一定的条件下,可能A线程需要等待,B线程在其他条件,又可能需要让A线程恢复。
方法如下:
使用的前提条件:必须是在synchronize关键字作用的代码块内。
满足一定的条件,加锁的对象.wait()----让当前线程等待(提前释放对象锁);
加锁对象.notify()----同一个锁对象,调用wait()进入等待状态的进程,随机唤醒一个,再次竞争对象锁;
加锁对象.notifAll()----同一个锁对象,调用wait()进入等待状态的进程,全部唤醒,让这些线程再次竞争对象锁。
10.线程池
作用:不用每次执行任务时都创建线程(会真实创建系统级别的线程,比较耗时,销毁也是),而是可以使用线程池中的线程来复用。
优点:减少每次启动,销毁线程的消耗。
11.停止线程池
可以通过调用线程池的ShutDown或ShutDownNow方法来关闭线程池。
原理:遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程(只是修改中断标志位,至于要不要中断,提交的任务代码自行决定),所以无法响应中断的任务可能永远无法终止。
ShutDown():中断队列中的任务,工作线程正在执行的任务,还是会执行完毕才能关闭线程池;
ShutDownNow():中断队列中的任务,及工作线程正在执行的任务。
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
进程与线程的区别
- 进程是系统进行资源分配的最小单位,线程是CPU调度的最小单位
- 进程有自己的内存地址空间,线程只独享指令执行的必要资源,如寄存器和栈
- 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信(进程数据传输需要通信)
- 线程的创建,切换及终止效率更高
wait和sleep的对比
- wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求
- wait 是 Object 的方法
- sleep 是 Thread 的静态方法