内容简介
并发变成的目的是为了让程序运行更快,合理利用CPU资源。
但是进行并发编程时,我们必须要了解多线程并发中容易存在的问题,比如:上下文切换、死锁、资源等问题。
1.1. 上下文切换
CPU处理任务时并不是一直处理当前的任务,而是通过给每个线程分配CPU时间片,当前线程处理的时间超过分配的时间片,就切换下一个线程,时间片极短,一般只有几十毫秒,所以CPU通过不停的切换线程执行时,直观感觉是线程同时在执行。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。在切换前会保存上一个任务的状态,用于重新调度这个任务时,可以直接加载这个任务的状态。所以
任务从保存到再加载的过程就是一次上下文切换
。
1.1.1 并发执行与串行执行孰优孰劣
public class Test {
private static long count = 10000000000L;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(() -> {
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
});
thread.start();
Thread thread2 = new Thread(() -> {
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
});
thread2.start();
thread.join();
thread2.join();
System.out.println("concurrency :" + (System.currentTimeMillis() - start) + "ms");
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
System.out.println("serial :" + (System.currentTimeMillis() - start) + "ms");
}
}
循环次数 | 并发执行耗时/ms | 串行执行耗时/ms |
---|---|---|
1万 | 45 | 0 |
1百万 | 49 | 4 |
1亿 | 84 | 70 |
10亿 | 391 | 694 |
100亿 | 3443 | 6453 |
当循环次数不超过1亿时,并发执行效率低于串行执行,因为线程的创建、销毁和上下文切换都会有额外的开销。
如何减少上下文切换
1.无锁并发编程,多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据,例如:ConcurrentHashMap就是采用锁分段技术。
2.CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁,java.util.concurrent.atomic.*
。
3.使用最少线程。避免创建不需要的线程,根据业务需求控制核心线程数,避免没有必要的资源浪费。
4.协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
1.2 死锁
下面我们来演示一下死锁出现的场景:
public class DeathLockObject implements Runnable {
private String userName;
private Object lock1 = new Object();
private Object lock2 = new Object();
void setUserName(String userName) {
this.userName = userName;
}
@Override
public void run() {
if ("a".equals(this.userName)) {
synchronized (lock1) {
try {
System.out.println("thread name :" + Thread.currentThread().getName()
+ " UserInfo :" + this.userName);
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (lock2){
}
}
}
if ("b".equals(this.userName)) {
synchronized (lock2) {
try {
System.out.println("thread name :" + Thread.currentThread().getName()
+ " UserInfo :" + this.userName);
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (lock1){
}
}
}
}
public static void main(String[] args) {
try {
DeathLockObject lockObject = new DeathLockObject();
lockObject.setUserName("a");
Thread threada = new Thread(lockObject);
threada.start();
Thread.sleep(100);
lockObject.setUserName("b");
Thread threadb = new Thread(lockObject);
threadb.start();
}catch (Exception e){
e.printStackTrace();
}
}
}
线程1和线程2同时在等待对方释放锁,造成死锁的出现。
多线程场景中一旦出现了死锁,业务上是可以感知的,具体分析是什么地方造成死锁的时候,我们可以使用
jps
和jstack
来协助排查。
示例如下:
1.通过jps -l
查看当前正在运行的Java进程
C:\Users\hp>jps -l
6912
16820 sun.tools.jps.Jps
24216 com.ykc.part16.DeathLockObject
16140 org.jetbrains.jps.cmdline.Launcher
26508 part16.Test
分析会出现死锁的进程,使用jstack -l
24216查看:
···
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001c02b968 (object 0x000000076b7f0118, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001c0290d8 (object 0x000000076b7f0128, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.ykc.part16.DeathLockObject.run(DeathLockObject.java:41)
- waiting to lock <0x000000076b7f0118> (a java.lang.Object)
- locked <0x000000076b7f0128> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.ykc.part16.DeathLockObject.run(DeathLockObject.java:27)
- waiting to lock <0x000000076b7f0128> (a java.lang.Object)
- locked <0x000000076b7f0118> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
如上所示, Found one Java-level deadlock:
JVM已经告诉我们出现了一处死锁的出现,Java stack information for the threads listed above:
通过下面对的堆栈信息,我们可以具体分析是什么地方造成了死锁。
避免死锁的几个常见方法。
1.避免单个线程中锁嵌套(需要持有多个锁)。
2.加锁顺序:若必须获取多个锁,在设计时,需要充分考虑线程获得锁的顺序。
3.尝试使用定时锁:当一个线程在尝试获取锁的过程中超过时限,该线程应该放弃对该锁进行请求,例如:lock.tryLock(timeout)
。
1.3 资源限制
1.线程并发执行时,收集受限于硬件、软件或网络等资源的限制,例如:CPU、硬盘、数据库连接数等等,对于硬件资源的限制,可以使用集群并行执行;对于软件资源的限制,可以将资源池复用。。
2.将代码中串行执行的部分改为并发执行后,由于受限于资源,仍然在串行执行,这时候的性能反而会更差,因为增加了上下文切换、线程的创建销毁、资源调度的时间。
3.根据不同的资源限制调整程序的并发度。