我们都知道CPU为了解决缓存一致性的问题,用了MESI协议,虽然它很好的解决了缓存一致性的问题。但是他却暴露一个很大的问题——缓存命中率下降。
如上所说,来来回回的让彼此的数据失效,导致两个CPU捞的数据都是在内存中取得,虽然保证了缓存一致性,却失去的一定的性能。这就是所谓的伪共享问题
无论是算法还是说明,计算机解决性能的问题常用的手段就是空间换时间,如下图所示,我们既然都知道CPU的cache line是64k,那么我们为什么不让a占一个cache line大小的内存,b也占一个cache line的内存。
这样CPU1加载cache line只会加载到一个变量,这样就保证了双核的计算机,只要不把这个从缓存中移出,数据的加载都会在cache中读取,性能就得到了保证。
如下所示,这段代码就是要做的很简单,就是开四个线程,将一个长度的4的数组对象的value元素设置为0,读者不妨尝试将带有屏蔽此行
的成员变量注释再解注释看看效果。
可以发现添加注释后的运行时间为:6573820850
去掉注释后的时间为:15142851950
public class FalseShareTest implements Runnable {
public static int NUM_THREADS = 4;
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] volatileLongs;
public static long SUM_TIME = 0l;
public FalseShareTest(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
Thread.sleep(10000);
for (int j = 0; j < 10; j++) {
System.out.println("第i次循环" + j);
if (args.length == 1) {
// 设置线程数为4
NUM_THREADS = Integer.parseInt(args[0]);
}
//搞四个避免伪共享的填充物
volatileLongs = new VolatileLong[NUM_THREADS];
for (int i = 0; i < volatileLongs.length; i++) {
volatileLongs[i] = new VolatileLong();
}
final long start = System.nanoTime();
//运行正在干活的逻辑
runTest();
final long end = System.nanoTime();
SUM_TIME += end - start;
}
System.out.println("平均耗时:" + SUM_TIME / 10);
}
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseShareTest(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
public void run() {
long i = ITERATIONS + 1;
//将每个数组里面的value设置为0
while (0 != --i) {
volatileLongs[arrayIndex].value = i;
}
}
public final static class VolatileLong {
//value是多个线程要操作的数据,并用volatile保证可见性
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; //屏蔽此行
}
}
原因也很简单,我们都知道long类型占了8个字节,如果添加了注释,那么四个线程要操作的数组volatileLongs
将会被加载到同一个cache line中,如下图所示,试想一下一个线程操作value使得其他cpu中的cache line失效的场景:
一旦解开注释那么cache line可能就会变这样,空间换时间,伪共享问题就解决了。
ava1.8以前一般都是填充解决. 而java1.8引入了 @Contended 注解(sun.misc.Contended).如下所示,我们要操作CacheLineTest 两个long变量,我们可以使用Contended使得这俩成员变量在加载的时候,存到不同的cache line中从而避免伪共享问题。
public class CacheLineTest {
@Contended
volatile long si;
@Contended
volatile long sj;
public static void main(String[] args) throws Exception{
for (int i = 0; i < 10; i++) {
CacheLineTest c=new CacheLineTest();
Thread t1=new Thread(()->{
for (int j = 0; j < 1000_000_00; j++) {
c.si=j;
}
});
Thread t2=new Thread(()->{
for (int j = 0; j < 1000_000_00; j++) {
c.sj=j;
}
});
final long start=System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
final long end=System.nanoTime();
System.out.println((end-start)/1000_000);
}
}
}
如下图所示,无论是进程还是线程,在Linux系统中统统称为task_struct 结构体
,只不过线程共享了进程的内存地址空间、文件描述、代码段等,所以线程也被称为轻量级的进程。
而Linux内核里的调度器,都是以task_struct 为单位调度一个个任务的。
而Linux按照优先级的分类,可将一个个task_struct 任务分为实时任务和普通任务。
实时任务:对响应要求较高,一般优先级在0-99,数字越小,优先级越高。
普通任务:就是可以正常安排的任务,优先级一般在100-139,数字越小,优先级越高。
Deadline 和 Realtime 这两个调度类,都是应用于实时任务的,这两个调度类的调度策略合起来共有这三种,它们的作用如下:
SCHED_DEADLINE
:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;SCHED_FIFO
:对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任务,可以抢占低优先级的任务,也就是优先级高的可以「插队」;SCHED_RR
:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;而 Fair 调度类是应用于普通任务,都是由 CFS 调度器管理的,分为两种调度策略:
SCHED_NORMAL
:普通任务使用的调度策略;SCHED_BATCH
:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。我们日常在Linux中跑的应用程序基本都是普通任务,Linux对这些任务设计了一个比较公平的算法,我们称之为CFS算法,即可完全公平调度(Completely Fair Scheduling)
他的工作原理也很简单,就是对每个任务都设置一个虚拟运行时间vruntime
,运行越久这个值越大,CFS在每次运行时会优先选择虚拟运行时间少的运行。这样就尽可能的保证了公平性。
在公平的情况下,我们有时候也需要考虑优先级,所以Linux还增加了一个nice级别的关键字段,nice设的越小,权重就越大,而权重和虚拟运行时间的关系公式如下所示
虚拟时间vruntime+=实际时间*NICE_0_LOAD(这个东西可不管当作固定值即可)/权重
从上公式可以看出,当我们nice值设置越小,权重就越大,那么vruntime就越小,优先级就越高。
我们都知道操作系统任务不可能一次性运行完,所以运行不完的任务都会被存到队列中,对于不同优先级别的任务就会放到不同的队列中。
如下所示Deadline 的任务放到dl_rq, Realtime任务放到rt_rq,而 Fair任务放到csf_rq中,操作系统拿任务时也是按照Deadline > Realtime > Fair这样的顺序取任务跑的。
例如我们要将普通应用的mysql优先级调高,我们就可以键入如下命令
nice -n -20 /usr/sbin/mysqld
上述命令意思也很简单,假如mysql任务级别是139那么减20,优先级就是100,就达到普通任务中的最高优先级了。
renice -10 -p [进程pid]
nice调整的是普通任务,如果你想任务搞成实时任务,我们可以使用如下命令,即将任务搞成实时任务,并且优先级为1,策略为SCHED_FIFO,先来先服务
chrt -f 1 -p [进程pid]
2.5 CPU 是如何执行任务的?
伪共享(false sharing),并发编程无声的性能杀手
java中解决伪共享问题