一、多线程的两个主要方向
并发:多线程之间各自执行各自的互不影响
并行:多线程之间互相竞争资源,进行读写的时候可能会产生相互覆盖
二、上下文切换
1.什么是上下文切换
在多线程编程中一般线程的个数都大于cpu的核心数,而一个cpu核心在任意时刻都只能被一个线程使用,CPU通过时间片分配算法来循环执行线程,当前线程执行完一个时间片后会切换到下一个线程。但是,在切换前会需要保存上一个线程的状态,以便下次切换回这个线程时,可以再加载这个线程的状态。所以线程从保存到再加载的过程就是一次上下文切换。
CPU分配给各个线程的时间非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几到几十毫秒(ms)。windows系统时不断的变换的,linux系统时固定的。那么cpu每秒都几十次到上百次的线程切换,而每次切换都需要纳秒量级的时间。所以上下文切换对于系统来说意味着消耗大量的cpu时间。事实上上下文切换可能是操作系统中时间消耗最大的操作。
2.代码并发执行一定比串行执行快吗?
①:什么是串行执行
一个时间段内,执行一个任务的同时不能执行其他任务,只能等到第一个任务完成后才能进行第二个任务,就这样任务一个接一个的执行就是串行。
②:什么是并发执行
一个时间段内,多个任务可以同时执行,各自的互不影响。
③:并发执行一定比串行执行快吗?
一个时间段内,串行执行是任务一个个的执行,并发执行是多个任务同时执行,那么并发执行一定比串行执行快吗?
首先定义一个任务,我们让两个线程去同时执行!
public class ConcurrencyTest {
private static final long count = 10000L;
public static void main(String[] args) throws Exception{
concurrentcy();;
serial();
}
private static void concurrentcy() throws Exception {
long start = System.currentTimeMillis();
Thread thread = new Thread(() -> {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
thread.join();
System.out.println(String.format("concurrecy: %dms, b=%d", time, b));
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b --;
}
long time = System.currentTimeMillis() - start;
System.out.println(String.format("serial: %dms, b=%d", time, b));
}
}
执行结果:
当并发执行循环操作不超过百万次时,并发执行速度会比串行执行操作要慢。 但是一旦循环超过百万,那么明显并发执行的效率会更高!
那么造成这种情况的原因是什么呢?
由于我们使用的是多核cpu,thread线程和main线程分别会多核cpu的不同的内核当中运行。这两个任务就是并发执行,各自的互不影响,那么者就属于并发。但是我们要注意由于时间片一般是几到几十毫秒(ms)这点时间无论是在哪个线程当中都是不足以支撑任务执行10000次,那么必然会涉及到线程间的上下文切换。
在serial()方法当中,只有main线程指向a+=5和b–两个任务每个任务分别指向10000次,而且由于程序执行顺序的原因,b–的任务只能等待a++任务的完成,这两个任务就是典型的串行执行。 多核CPU在执行单线程任务时,通常不会发生线程间的上下文切换。 上下文切换主要发生在操作系统需要从一个线程切换到另一个线程时,这通常涉及保存当前线程的状态并恢复下一个线程的状态。然而,在单线程环境下,只有一个线程在执行,因此没有线程间的切换需求。
总结:
由于上下文切换涉及到保存当前任务的执行状态(如CPU寄存器的内容、程序计数器等),并将这些状态信息存储到内存中,然后加载下一个任务的执行状态到CPU中,使其能够继续执行。这个过程需要一定的时间,并且涉及到CPU和内存之间的数据传输,可能会产生额外的开销。频繁的上下文切换会消耗大量的CPU时间和内存资源,从而可能降低整体的执行效率。特别是在处理大量短任务或者任务之间的切换开销相对较大的情况下,并发执行会因为过多的上下文切换而比串行执行更慢。
三、减少上下文切换实战
减少上下文切换的方法: 无锁并发编程、CAS算法、使用最少线程和使用协程。
(1) 无锁并发编程: 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同分段的数据。
(2) CAS算法: Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
(3) 使用最少线程: 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
(4) 使用协程: 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
多线程是无序的,但是协程是有序的,一个线程下边可以创建多个协程,多个协程之间会按照一定顺序交替执行。
四、减少上下文切换实战
第一步:使用命令显示正在运行的java进程的进行ID
这里有两种命令,我们可以使用ps或jps命令来查看所有Java程序进程。ps命令是用于显示当前进程状态的命令,而jps命令是Java Virtual Machine Process Status Tool的缩写,用于显示所有Java进程的进程ID。
//使用ps命令查看所有java程序进程
ps aux | grep java
//使用jps命令查看所有java程序进程
jps
第二步:使用jstack命令dump(镜像)线程信息
jstack 命令是JDK工具之一,使用该命令可以打印正在运行中 Java 进程的栈信息。
我们这里选择ID是5131的线程,我们首先要进入到jdk的bin目录当中,在执行jstack命令,生成5131.dump文件
第三步:通过各种状态的线程数量
grep java.lang.Thread.State 5131.dump | awk '{print $2$3$4$5}' | sort | uniq -c
注意我这里没有那么多线程,所有只能借助其他的信息
统计所有线程分别处于什么状态,发现300多个线程处于WAITING(onobject-monitor)状态。
第四步:打开dump文件,查看处于WAITING(onobjectmonitor)的线程在做什么
我们发现大量线程都闲着。
第五步: 减少工作线程数,将maxThreads设置小一点
在tonmcat的server当中改变最大线程数的数量,将其减少!
第六步: 重启
重启统计WAITING(onobjectmonitor)数量,发现减少了很多,WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从WAITING到RUANNABLE都会进行一次上下文切换,可以用vmstat命令来查看上下文切换的次数,其中cs列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)。