循环次数 | 串行执行耗时/ms | 并发执行耗时/ms | 并发比串行快多少 |
---|---|---|---|
1万 | 0 | 5 | 慢 |
10万 | 2 | 3 | 慢 |
100万 | 3 | 4 | 差不多 |
1000万 | 8 | 7 | 差不多 |
1亿 | 54 | 54 | 差不多 |
10亿 | 514 | 508 | 差不多 |
从上表可以看出,当并发执行累计操作低于百万次时,速度会比串行执行累加操作要慢。为什么在这种情况下并发执行比串行执行要慢呢?这是因为创建线程和上下文切换的时间开销要远远大于简单计算的时间开销。
1.2 测试上下文切换次数和时长
测试工具:
- 使用Lmbench3可以测量上下文切换的时长
- 使用vmstat可以测量上下文切换的次数
vmstat参数的含义:
参数名 | 含义 |
---|---|
r | 表示运行队列(就是说多少个进程真的分配到CPU) |
b | 表示阻塞的进程 |
swpd | 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。 |
free | 空闲的物理内存的大小 |
buff | Linux/Unix系统用来存储,目录里面有什么内容,权限等的缓存 |
cache | 用来记忆我们打开的文件,给文件做缓冲 |
si | 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉 |
so | 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上 |
bi | 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte |
bo | 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整 |
in | 每秒CPU的中断次数,包括时间中断 |
cs | 每秒上下文切换次数 |
us | 用户CPU时间 |
sy | 系统CPU时间,如果太高,表示系统调用时间长,例如IO操作频繁 |
wt | 等待IO CPU时间 |
# 每隔一秒采集数据,一直采集,直到程序终止
vmstat 1
CS(Content Switch)表示上下文切换的次数,从上面的可以看出上下文每秒钟切换1000多次。
1.3 如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的id按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程处于等待状态。
- 协程。在单线程里实现多任务调度,并在单线程里维持多个任务见的切换。
1.4 减少上下文切换实战
这个例子简单说明如何来减少线程池中大量WAITING线程,来减少上下文切换次数。(本文在Windows环境dump测试)
写一个模拟出现WAITING状态的代码:
package com.lizba.p1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/\*\*
\* <p>
\* 线程池Dump测试 -- 代码只是示例
\* </p>
\*
\* @Author: Liziba
\* @Date: 2021/6/4 23:26
\*/
public class ThreadPoolDumpTest {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(300);
// 初始化线程池中的线程
for (int i = 0; i < 300; i++) {
fixedThreadPool.execute(getThread(i));
}
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("测试!");
}
}
/\*\*
\* 创建线程
\* @param i
\* @return
\*/
private static Runnable getThread(final int i) {
return new Runnable() {
public void run() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
}
- 用jstack命令dump线程信息,可以看当前运行的Java程序的pid,查看当前进程号里的线程在做什么。
# 查看Java进程
jps
结果:
1216
12176 RemoteMavenServer36
18052 ThreadPoolDumpTest
18084 Launcher
15800 Jps
- 统计所有线程分别处于什么状态,找出处于(onobjectmonitor)阻塞状态的线程。
# dump下快照
jstack -l 18052 > d:\dump.txt
- 打开dump文件查看处于(onobjectmonitor)阻塞的线程在做什么。
发现有300个线程处于WAITING状态
"pool-1-thread-300" #311 prio=5 os\_prio=0 tid=0x000000002fe46800 nid=0x4880 waiting on condition [0x0000000033cfe000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000077b098178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
- 此时如果发现是我们在程序中定义的线程池中的线程,则我们应该适当考虑降低线程池的maxThreads的值。
此处示例中我们修改线程池的固定大小为10:
// 创建固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
- 修改maxThread值之后我们可以重启项目。再次dump线程信息,然后重新统计(onobjectmonitor)阻塞的线程数。
再次dump快照分析线程运行情况,发现只有10个线程处于WAITING状态了:
"pool-1-thread-10" #21 prio=5 os\_prio=0 tid=0x000000001ecde000 nid=0x312c waiting on condition [0x00000000212ef000]
java.lang.Thread.State: WAITING (parking)
在上面的简单案例中WAITING线程减少了,系统上下文切换的次数就会减少,因为每一次从WAITING到RUNNABLE都会进行一次上下文的切换。在实际开发中,我们并不会做这么看似低级的操作,但是样例却能给我们代理线程池优化和程序线程优化各方面的解决问题的思路。
二、死锁
锁是一个非常有用的工具,运用的场景非常多,因为它使用起来非常简单,而且易于理解。但同时它会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。
2.1 死锁示例
下面演示一段引起死锁的代码,使得线程t1和线程t2互相等待对方释放锁。
package com.lizba.p1;
/\*\*
\* <p>
\* 死锁示例代码
\* </p>
\*
\* @Author: Liziba
\* @Date: 2021/6/5 0:37
\*/
public class DeadLockDemo {
private static final String A = "A";
private static final String B = "B";
/\*\*
\* t1\t2互相持有锁
\*/
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
public void run() {
// 持有锁A
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 持有锁B
synchronized (B) {
System.out.println("hold Lock B");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
// 持有锁B
synchronized (B) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 持有锁A
synchronized (A) {
System.out.println("hold Lock A");
}
}
}
});
t1.start();
### 文末
如果30岁以前,可以还不知道自己想去做什么的话,那30岁之后,真的觉得时间非常的宝贵,不能再浪费时间在一些碎片化的事情上,比如说看综艺,电视剧。一个人的黄金时间也就二,三十年,不能过得浑浑噩噩。所以花了基本上休息的时间,去不断的完善自己的知识体系,希望可以成为一个领域内的TOP。
同样是干到30岁,普通人写业务代码划水,榜样们深度学习拓宽视野晋升管理。
这也是为什么大家都说30岁是程序员的门槛,很多人迈不过去,其实各行各业都是这样都会有个坎,公司永远都缺的高级人才,只用这样才能在大风大浪过后,依然闪耀不被公司淘汰不被社会淘汰。
**269页《前端大厂面试宝典》**
包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
![](https://img-blog.csdnimg.cn/img_convert/b98713ee557d94286de8afe404cb51d1.png)
**前端面试题汇总**
![](https://img-blog.csdnimg.cn/img_convert/1d691ca297c9016828aff783a701e065.png)
**JavaScript**
![](https://img-blog.csdnimg.cn/img_convert/a489904576da281d07092f043566f6dd.png)