我们平时写的程序默认都是由一个main函数进入,到函数执行完毕,那么我们的程序也就结束了。这个过程其实是了一个进程(即应用程序)被读入内存中,直到执行完毕后移除内存的整个过程。不管是从前的单核还是如今的性能越来越好的多核计算机,在一个进程中能够“并发”的执行多个线程都是及其重要的。在单CPU时代,由多个线程来模拟“并发”可以让一些程序执行得更合理,比如说在浏览器中一个线程处理文字,另外一个线程并行的处理图片,从而实现每个线程处理好自己的内容以后显示在浏览器上而不是需要所有内容的处理好一起显示,这让交互变得更加友好,并且显得“速度更快”。
在如今的多CPU时代,利用好并发,学好并发编程,绝对可以让我们的程序运行得更加流畅,用户体验更佳。之前在《Java编程思想》中对并发有了一定的了解,由于目前是开发移动端程序,相对来说并发量较低,对并发编程的能力要求也不是太高,会利用一些基本的并发API、对进程、线程有一个基本的认识也就足矣。但是作为一个“热爱学习、热爱钻研”的程序员来说,并发编程绝对是我们需要深入了解,结合计算机的其他知识来理解的一套思想。最近入手了一本《Java并发编程艺术》,想一边看一边实践并且一边记录下自己学习的内容,以供参考。
我们知道,CPU是一个不断取指执行的运算机器,而每个线程维护了自己的栈信息,当从一个线程切入到另外一个线程执行得时候,CPU需要用一个结构体记录前一个线程的相关信息(变量、方法、返回地址等栈内容),然后再切换到另外一个线程执行,这就是一个上下文切换的过程,而这个过程是需要一定开销的。
看一个例子,在Android中,主线程执行UI相关操作,而所有网络请求都要通过子线程来进行。这造成了一些人误以为除了UI的所有操作都通过子线程,执行结果需要改变UI再切换回主线程来直线,这就造成了不必要的上下文切换开销。也就是说,在轻量级的操作上,直接在主线程执行其实是比切换线程对系统资源的消耗更小的。下面是一个上下文切换的例子:
public class ConcurrencyTest {
private static final long count = 100000000l;
public static void main(String... args) throws InterruptedException {
concurrency();
serial();
}
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();
System.out.println("serial : " + (time - start) + "ms,b=" + b);
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
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--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concurrency : " + time + "ms,b=" + b);
}
}
| 循环次数 | 串行执行耗时/ms | 并发执行耗时 | 并发比串行快多少 |
|---|---|---|---|
| 1万 | 0 | 3 | 慢 |
| 10万 | 3 | 3 | 速度相同 |
| 100万 | 4 | 3 | 慢25% |
| 1000万 | 10 | 8 | 快25% |
| 10000万 | 67 | 36 | 快1倍 |
从上面的例子可以看出,上下文切换的开销是不可以忽略的,在不涉及到IO仅仅是运算的CPU操作上,完全没有必要进行线程的切换,甚至会适得其反。
- 那么如何减少切换上下文呢?
- 无锁并发编程: 多线程竞争锁时,会引起上下文切换,所以多线程处理数据的时候,可以采用一些办法来避免使用锁,如将数据的ID按照Hash算法分段,不同的线程处理不同的数据。(这和操作系统对内存的管理有一些相似,即内存隔离)
- CAS算法,Java的Atomic包使用CAS算法来更新数据,不需要加锁。
- 使用最少线程。避免创建不必要的线程,线程池也得有一定的大小,可以用一个BlockingQueue来维护事件。
- 协程:在单线程里实现多任务的调度,并在单线程维持多个任务切换。
至此,对上下文的讨论就完了,后续有补充会继续加进来。
本文探讨了并发编程中的上下文切换,解释了它在单CPU和多CPU时代的重要性。上下文切换带来了性能开销,特别是在不必要的线程切换中。通过无锁并发编程、CAS算法、限制线程数量以及使用协程,可以有效减少这种开销。举例说明了Android中避免主线程和子线程不当切换的策略,强调在轻量级操作中减少上下文切换以提高效率。
171

被折叠的 条评论
为什么被折叠?



