Java并发-CPU如何实现的多线程

时间片

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并发编程的艺术>>