概述
并发编程的目的是为了让程序运行更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战,比如上下文切换、死锁的问题,以及受限于硬件和软件的资源限制问题。
上下文切换
单核处理器也支持多线程执行代码,CPU 通过给每个线程分配 CPU 时间片来实现这个机制。时间片是 CPU 分配给各个线程的时间,因为时间片非常短,所以 CPU 通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒。
那如何减少上下文切换呢?有以下几种办法:
-
无锁并发编程
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 hash 算法取模分段,不同的线程处理不同段的数据。
-
CAS 算法
Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
-
使用最少线程
避免创建不需要的线程。
-
协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
死锁
死锁产生的四个必要条件:
-
互斥使用
即当资源被一个线程占有时,别的线程不能使用。
-
不可抢占
资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
保持和请求
即当资源请求者在请求其他资源的同时保持对原有资源的占有。
-
循环等待
即存在一个等待队列:P1 占有 P2 的资源,P2 占有 P3 的资源,P3 占有 P1 的资源,这样就形成了一个等待环路。
死锁的处理策略:
-
死锁预防
破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部的资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防死锁通常会降低系统的效率。
-
死锁避免
避免是指进程每次申请资源时判断这些操作是否安全,例如使用银行家算法,死锁避免算法的执行会增加系统的开销。
银行家算法
当一个线程申请使用资源的时候,银行家算法通过先 试探 分配给该线程,然后通过安全性算法来判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该线程继续等待。
-
死锁检测
判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
-
死锁解除
这是与死锁检测结合使用,它使用的方式就是剥夺。即将某进程所拥有的资源强行回收,分配给其他进程。
死锁示例代码:
private static final Object object1=new Object();
private static final Object object2=new Object();
private static void deadLock() {
Thread thread1 = new Thread(() -> {
synchronized (object1) {
try {
Thread.sleep(1000);
synchronized (object2) {
System.out.println("thread1----------");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (object2) {
synchronized (object1) {
System.out.println("thread2----------");
}
}
});
thread1.start();
thread2.start();
}
资源限制的挑战
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行执行的代码并行执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
网友评论