并发编程的挑战主要是在三个方面
- 上下文切换
- 死锁
- 资源限制
下面就这三个方面进行分别分析遇到的挑战以及如何应对。
1)上下文切换
1.1 什么是上下文切换?多线程一定快么?
想了解上下文切换,我们先来了解几个概念。我们都知道在一块CPU上进行多线程,实际上是CPU通过时间分配算法给每个线程分配了不同的时间片,时间片就是CPU分配给每个线程的执行时间,由于时间片的单位量级是几十毫秒,因此我们感觉到多个线程在同时执行。但实际上,从当前任务切换到下一个任务之前,会保存上一个任务的状态,以便下次切换到这个任务时候,可以直接加载它上次的状态。
因此上下文切换指的就是任务从保存到再加载的过程就是一次上下文切换。但是由于这个切换需要进行状态的保存,因此是需要代价的,会影响效率。
因为存在上下文切换,因此多线程执行不一定不单线程快。
package chanllenge;
public class ConcurrencyTest {
private static final long count = 10000L;
public static void main(String[] args) {
concurrency();
serial();
}
private static void serial() {
// TODO Auto-generated method stub
long start = System.currentTimeMillis();
int a = 0;
for(long i=0;i<count;i++ ){
a+=5;
}
int b = 0;
for(long j=0;j<count;j++){
b--;
}
long time = System.currentTimeMillis()-start;
System.out.println("serial :"+time+"ms,b="+b+",a="+a);
}
private static void concurrency() {
// TODO Auto-generated method stub
// TODO Auto-generated method stub
long start = System.currentTimeMillis();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int a = 0;
for(long i=0;i<count;i++ ){
a+=5;
}
}
});
t.start();
int b = 0;
for(long j=0;j<count;j++){
b--;
}
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
long time = System.currentTimeMillis()-start;
System.out.println("concurrency :"+time+"ms,b="+b);
}
}
如上代码,最终执行结果为:
concurrency :2ms,b=-10000
serial :0ms,b=-10000,a=50000
显然,此时多线程执行的反而慢。
1.2 如何应对上下文切换的挑战?
了解了什么是上下文切换,我们只要从原理上入手,就很容易知道怎么应对上下文挑战——尽可能的减少线程或让线程分工明确,减少进行切换。如下是减少上下文切换的一些方法。
1.2.1 无锁并发编程
多线程竞争锁的时候,会引起上下文切换。所以我们可以采用分段处理数据等方式来避免使用锁。
1.2.2 CAS算法
Java的Atomic包使用CAS算法来更新数据,不需要加锁。
1.2.3 使用最小线程
避免创建不需要的线程,比如任务很少,就不需要创建很多线程。
1.2.4 协程
在单线程里实现多任务调度,并在单线程中维持多个任务间的切换。
2)死锁
死锁,是大家多线程中一个基础的概念,在此不再赘述,仅简述避免死锁的一些方法。
2.1 避免一个线程同时获取两个锁
2. 2 避免一个线程在锁内占用多个资源
2.3 尝试使用定时锁
2.4 对于数据库锁,加锁解锁必须在一个数据库链接中
3) 资源限制
资源限制又分为硬件,软件。硬件资源限制有服务器带宽,硬盘读写速度,CPU处理速度。软件资源限制有数据库链接数、socket链接数等。
原则上我们使程序加快速度方式为使串行变并行,但是如果受限于资源,串行反而会比并行执行的更快。
应对资源限制,就是靠money啊啊啊啊啊