一、进程和线程的概念
1、进程
进程是操作系统进行资源分配的最小单元,资源例如:cpu,内存,磁盘IO。进程之间是独立的。例如我们在windows上安装的一个PPT程序叫应用程序,当我们启动我们的PPT程序,操作系统中就会存在一个PPT的应用程序进程。
2、线程
线程是CPU调度的最小单位,必须依赖于进程存在,不能独立存在。一个进程可以拥有多个线程,线程可以共享进程的资源,包括内存、磁盘IO。
二、CPU核心数和线程数的关系
通过下图看到我们计算机的一个配置信息:内核是4个,逻辑处理器是8个,也就意味着CPU核心数是4个,并且我们的计算机同时可以跑8个线程。
image.png三、并行和并发的概念
1、并行
并行就是指可以同时运行的任务数。可以理解为操作系统同时可以运行的线程数。例如以上我们计算机的配置,核心线程数是8,意思就是并行数是8。
image.png
2、并发
并发指的是:某个时间内可以执行的任务数。时间片轮转其实就是一种并发机制。例如我们的并行数虽然是8个,但是由于CPU时间片的轮转,我们1S内执行了100个线程,那我们就可以理解成我们的1S内的并发是100。
image.png3、并发编程的好处
采用多线程编程的好处:充分利用CPU的资源,加快响应用户的时间。代码模块化,异步化。
需要注意:线程共享进程的资源,并发编程就导致线程抢占资源。使用锁,也可能会导致死锁等等。
四、线程的基本状态
线程基本状态有7种。
image.png
- 创建一个线程Thread的时候,线程处于初始状态
- 调用Thread.start的时候,线程处于就绪、运行状态,就绪状态指的是等待CPU分配时间片,当拿到时间片的时候,就处于运行状态
- 调用wait、join、LockSupport.park方法后,线程处于等待状态
- 调用waite、sleep相关传入时间,线程处于等待超时状态
- 当线程调用notify被唤醒,或者线程等待超时结束之后,线程继续进入就绪、运行状态
- 当线程调用synchronized修饰的代码块,没有拿到锁的时候,线程进入了阻塞状态
- 当线程执行完毕,线程处于终止状态
六、上下文切换
1、什么是上下文切换
我们知道在处理多线程并发任务的时候,处理器会为每一个线程分配CPU时间片,线程在各自的时间片内执行任务。每个时间片大概几十毫秒,所以在1秒钟内可能会发生几十上百次的线程相互切换。因为切换的速度比较快,所以我们会感觉线程是同时执行的。
线程只在CPU分配的时间片内执行任务,当一个线程的时间片用完了,或者自身因素被迫暂停运行的时候,就会有另外一个线程来占用这个CPU。这种一个线程让出CPU使用权,另外一个线程获取占用CPU使用权的过程就叫做上下文切换。
一个线程让出CPU使用权就是切出,另外一个线程获取占用CPU使用权就是切入。这个切出切入的过程中,操作系统会保存和恢复线程的相关进度的信息。这个进度信息我们就称之为上下文。上下文一般包含了寄存器存储的内容和程序计数器的指令内容。
2、上下文切换的原因
在多线程编程中我们知道上下文的切换会造成性能问题,那么是什么原因造成上下文切换呢?
image.png
我们先来回顾线程的几种状态:
(1)当一个线程从就绪状态切换到运行中状态的时候,就是上下文的一次切换。
(2)从运行中状态到阻塞状态、等待状态、等待超时状态,从这些状态再到就绪状态,再从就绪态再到运行中状态,又是一次上下文的切换。
参考右图,线程从运行中状态到阻塞状态、等待状态、等待超时状态的时候,我们叫做线程暂停。当线程暂停的时候,就会让出CPU的使用权,这个CPU就会有别的线程来占用,这个时候操作系统就会保存上下文的信息,方便这个线程再次运行的时候在原来的进度执行。当线程从暂停到就绪状态的时候,我们称为线程的唤醒,此时操作系统就会恢复之前保存的上下文信息,当线程拿到时间片后在原来的上下文继续执行,变为运行中状态。
3、上下文切换的类型
(1)自发性上下文切换
自发性上下文切换是由JAVA程序调用导致切出的,一般是在编码的时候,调用以下几个方法:
sleep()
wait()
yield()
join()
park();
synchronized
lock
(2)非自发性上下文切换
非自发性上下文切换一般包含:线程分配的时间片用完、虚拟机垃圾回收、或者线程的执行优先级。
4、多线程执行速度一定大于单线程吗?
代码演示线程的上下文切换以及性能测试
package cn.enjoyedu.concurrent.cotext;
public class TreadConcurrentTest {
private static final long count = 10000;
public static void main(String[] args) throws Exception {
{
current();
serial();
}
}
/**
* 多线程并行执行
* @throws Exception
*/
private static void current() throws Exception {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
public void run() {
int a = 0;
for (int i = 0; i < count; i++) {
a += 5;
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
}
});
//启动线程1 和2
thread.start();
thread2.start();
//阻塞主线程
thread.join();
thread2.join();
long time = System.currentTimeMillis() - start;
System.out.println("发执行时间:" + time + "ms");
}
/**
* 单线程串行执行
*/
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (int i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("串行执行时间:" + time + "ms");
}
}
发执行时间:2ms
串行执行时间:1ms
以上结果并发执行时间大于单线程串行执行时间。
在之前我们提到了并发编程的好处,但是并发编程优点并不是绝对的。多线程并发意味着要对线程的创建和销毁,对操作系统带来一定的开销。同时多线程带来的上下文切换也会带来性能的开销,主要体现在保存和恢复上下文信息。
一般情况下执行任务较少的时候,我们采用单线程,执行任务较多的时候,可以考虑采用多线程。
5、如何避免频繁的上下文切换?
(1)在实际开发中,我们有时候需要加锁,所以对于加锁我们需要注意,加锁和释放锁会导致比较多的上下文切换以及调度延时。引起性能问题。实际开发中需要注意。
(2)创建过多的线程,如果创建线程太多,线程切换的速度大于线程的执行速度。一般情况下创建的数量控制在CPU的核心线程数。
网友评论