时间片
CPU通过给现成分配时间片的形式来实现多线程。所以单核CPU也支持多线程执行。
多个线程看起来像同时运行,实则是轮番运行,由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。
上下文切换
CPU通过时间片分配算法来循环执行任务。切换时间片的同时,会保存上一个执行线程任务的状态,用于切回。从切换出到切换回,算作一次上下文的切换。
比如,从英语课下课切换成数学课,老师和同学需要记住上节英语课上到哪一节了。上完其他课程再上英语课,就接着上一节讲到的位置接着上。
但是,如果一直上英语课不切换成其他课程,学英语的效率就会变得更高,因为节省了思维切换的开销。
同理,多线程在上下文切换时,也会造成开销。这就导致多线程在某些情况下,执行速度并不会比单线程要快。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class ConcurrencyTest { private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException { concurrency(); serial(); }
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--; } long time = System.currentTimeMillis() - start; thread.join(); System.out.println("concurrency :" + time + "ms,b=" + b); }
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() - start; System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a); } }
|
执行结果:
1 2
| concurrency :2ms,b=-10000 serial:0ms,b=-10000,a=50000
|
可见,多线程效率反而低了,这便体现了上下文切换所造成的多余的耗费。
如何减少上下文的切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程:
多线程竞争锁时,会引起上下文切换,
可将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据,可以避免使用锁。
CAS算法:
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
避免创建不需要的线程:
比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
协程:
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
参考
<<Java并发编程的艺术>>