java在CPU中的一些个破事

其实写java的人貌似和CPU没啥关系,最多最多和我们在前面提及到的如何将CPU跑满、如何设置线程数有点关系,但是那个算法只是一个参考,很多场景不同需要采取实际的手段来解决才可以;而且将CPU跑满后我们还会考虑如何让CPU不是那么满,呵呵,人类,就是这么XX,呵呵,好了,本文要说的是其他的一些东西,也许你在java的写代码时几乎不用关注CPU,因为满足业务才是第一重要的事情,如果你要做到框架级别,为框架提供很多共享数据缓存之类的东西,中间必然存在很多数据的征用问题,当然java提供了很多concurrent包的类,你可以用它,但是它内部如何做的,你要明白细节才能用得比较好,否则还不如不用,本文可能不是阐述这些内容作为重点,因为如标题党:我们要说CPU,呵呵。

还是那句话,貌似java和CPU没有多少关系,我们现在来聊聊有啥关系;

1、当遇到共享元素,我们通常第一想法是通过volatile来保证一致性读的操作,也就是绝对的可见性,所谓可见性,就是每次要使用该数据的时候,CPU不会使用任何cache的内容都会从内存中去抓取一次数据,并且这个过程对多CPU仍然有效,也就是相当CPU和内存之间此时是同步的,CPU会像总线发出一个Lock addl 0类似的的汇编指令,+0但相对于什么都不会做;不过一旦该指令完成,后续操作将不再影响这个元素其他线程的访问,也就是他能实现的绝对可见性,但是不能实现一致性操作,也就是说,volatile不能实现的是i++这类操作的一致性(在多线程下并发),因为i++操作是被分解为:

int tmp = i;
tmp = tmp + 1;
i = tmp;

这三个步骤来完成,从这点你也能看出i++为什么能实现先做其他的事情再自我加1,因为它讲值赋予给了另一个变量。

2、我们要用到多线程并发一致性,就需要用到锁的机制,目前类似Atomic*的东西基本可以满足这些要求,内部提供了很多unsafe类的方法,通过不断对比绝对可见性的数据来保证获取的数据是最新的;接下来我们继续来说一些CPU其他的事情。

3、以前我们为了将CPU跑满,但是无论如何跑不满,因为我们开始说了忽略掉内存与CPU的延迟,今天既然提及到这里,我们就简单说下延迟,一般来讲现在的CPU有三级cache,年代不同延迟不同,所以具体数字只能说个大概而已,现在的CPU一般一级cache的延迟在1-2ns,二级cache一般是几个ns到十来ns左右,三级cache一般是30ns到50ns不等,内存访问普遍会上到70ns甚至更多(计算机发展速度很快,这个值也仅仅在某些CPU上的数据,做一个范围参考而已);别看这个延迟很小,都是纳秒级别,你会发现你的程序被拆分为指令运算的时候,会有很多CPU交互,每次交互的延迟如果有这么大的偏差,此时系统性能是会有变化的;

4、回到刚才说的volatile,它每次从内存中获取数据,就是放弃cache,自然如果在某些单线程的操作中,会变得更加慢,有些时候我们也不得不这样做,甚至于读写操作都要求一致性,甚至于整个数据块都被同步,我们只能在一定程度上降低锁的粒度,但是不能完全没有锁,即使是CPU本身级别也会有指令级别的限制,如下:

5、在CPU本身级别的原子操作一般叫屏障,有读屏障、写屏障等,一般是基于一个点的触发,当程序多条指令发送到CPU的时候,有些指令未必是按照程序的顺序来执行,有些必须按照程序的顺序来执行,只要能最终保证一致即可;在排序上,JIT在运行时会做改变,CPU指令级别也会做改变,原因主要是为了优化运行时指令让程序跑得更快。

6、CPU级别会对内存做cache line的操作,所谓cache line会连续读一块内存,一般和CPU型号和架构有关系,现在很多CPU每次读取连续内存一般是64byte,早期的有32byte的,所以在某些数组遍历的时候会比较快(基于列遍历很慢),但这个并不完全对,下面会对照一些相反的情况来说。

7、CPU对数据如果发生了修改,此时就不得不说CPU对数据修改的状态,数据如果都被读取,在多CPU下可以被多线程并行读取并,当对数据块发生写操作的时候,就不一样了,数据块会有独占、修改、失效等状态,数据修改后自然就会失效,当在多CPU下,多个线程都在对同一个数据块进行修改时,就会发生CPU之间的总线数据拷贝(QPI),当然如果修改到同一个数据上的时候我们是没有办法的,但是回到第6点的cache line里面,问题就比较麻烦了,如果数据是在同一个数组上,而数组中的元素会被同时cache
line到一个CPU上的时候,多线程的QPI就会非常频繁,有些时候即使是数组上组装的是对象也会出现这个问题,如:

class InputInteger {
   private int value;
   public InputInteger(int i) {
      this.value = i;
   }
}
InputInteger[] integers = new InputInteger[SIZE];
for(int i=0 ; i < SIZE ; i++) {
   integers[i] = new InputInteger(i);
}

此时你看出来integers里面放的全部是对象,数组上只有对象的引用,但是对象的排布理论上说各自对象是独立的,不会连续存放,不过java在分配对象内存的时候,很多时候,在Eden区域是连续分配的,当在for循环的时候,如果没有其他线程的接入,这些对象就会被存放在一起,即使被GC到OLD区域也很有可能会放在一起,所以靠简单对象来解决cache line后还对整个数组修改的方式貌似不靠谱,因为int
是4字节,如果在64模式下,这个大小是24字节(有4byte补齐),指针压缩开启是16byte;也就是每次cpu可以看齐3-4个对象,如何让CPUcache了,但是又不影响系统的QPI,别想通过分隔对象来完成,因为GC过程内存拷贝过程很可能会拷贝到一起,最好的办法是补齐,虽然有点浪费内存,但是这是最靠谱的方法,就是将对象补齐到64字节,上述若未开启指针压缩有24byte,此时还有40个字节,只需要在对象内部增加5个long即可。

class InputInteger {
   public int value;
   private long a1,a2,a3,a4,a5;
}

呵呵,这个办法很土,不过很管用,有些时候,Jvm编译的时候发现这几个参数啥都没做,就直接给你干掉了,优化无效,土办法加土办法就是在一个方法体里面简单对这5个参数做一个操作(都用上),但是这个方法永远不调用它即可。

8、在CPU这个级别有些时候就未必能先做尽量先做的道理为王者了,类似获取锁这种操作,在AtomicIntegerFieldUpdater的操作,如果调用getAndSet(true)在单线程下你会发现跑得还蛮快,在多核CPU下就开始变慢,为什么上面说得很清楚了,因为getAndSet里面是修改后对比,先改了再说,QPI会很高,所以这个时候,先做get操作,再修改才是比较好的做法;还有就是获取一次,如果获取不到,就让步一下,让其他的线程去做其他的事情;

9、CPU有些时候为了解决某些CPU忙和不繁忙的问题,会有很多算法来解决,如NUMA是其中一种方案,不过不论哪种架构都在一定场景下比较有用,对有所有场景未必有效;有队列锁机制来完成对CPU状态管理,不过这又存在了cache line的问题,因为状态都是经常改变的,各类应用程序的内核为了配合CPU也会出一些算法来做,使得CPU可以更加有效的利用起来,如CLH队列等。

有关这方面的细节会很多如用普通变量循环叠加和用volatile类型的做以及Atomic*系列的来做,完全是不一样的;多维度数组循环,按照不同纬度向后次序来循环也是不一样的,细节上点很多,明白为什么就在实际优化过程中有灵感了;锁的细节说太细很晕,在系统底层的级别,始终有一些轻量级的原子操作,不论谁说他的代码是不需要加锁的,最细的可以细到CPU在每个瞬间只能执行一条指令那么简单,多核心CPU在总线级别也会有共享区来控制一些内容,有读级别、写级别、内存级别等,在不同的场景下使得锁的粒度尽量降低,那么系统的性能不言而喻,很正常的结果。

本文就说到这里,闲扯了下,仅供参考!

时间: 2024-10-29 13:59:26

java在CPU中的一些个破事的相关文章

Java应用程序中动态分配CPU资源

一个多任务系统需要在任务之间实现QoS(Quality of Service)管理时,如果CPU资源的分配基于Java线程的优先级,那么它在不同平台上运行时的效果是很难预测的. 本文利用协调式多任务模型,提出一个与平台无关.并且能在任务间动态分配CPU资源的方案. 现在,由于计算机系统已经从人机交互逐步向机机交互转化,计算机和计算机之间的业务对于时间的要求非常高.软件系统对于业务的支持已经不仅表现为对不同业务的逻辑和数据(算法+数据结构)支持,而且还表现为对同时处理不同任务的时效性(任务响应速度

聊聊并发(二)Java SE1.6中的Synchronized

本文属作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/java-se-16-synchronized 1 引言 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程. 2 术语定义 术语 英

详解Java多线程编程中线程的启动、中断或终止操作_java

线程启动: 1.start() 和 run()的区别说明start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法.start()不能被重复调用. run() : run()就和普通的成员方法一样,可以被重复调用.单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程! 下面以代码来进行说明. class MyThread extends Thread{ public void run(){ ... } }; MyThread mythread = new

Java线程并发中常见的锁--自旋锁 偏向锁

  随着互联网的蓬勃发展,越来越多的互联网企业面临着用户量膨胀而带来的并发安全问题.本文着重介绍了在java并发中常见的几种锁机制.     1.偏向锁 偏向锁是JDK1.6提出来的一种锁优化的机制.其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作.也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式.在JVM中使用-XX:+UseBiasedLocki

深入解析Java并发程序中线程的同步与线程锁的使用_java

synchronized关键字 synchronized,我们谓之锁,主要用来给方法.代码块加锁.当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码.当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段.但是,其余线程是可以访问该对象中的非加锁代码块的. synchronized主要包括两种方法:synchronized 方法.synchronized 块. synchron

解析Java的迭代器中的fast-fail错误检测机制_Android

fail-fast 机制是java集合(Collection)中的一种错误机制.当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件.例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了:那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件. fail-fast 机制是java集合(Collection)中的一种错误机制.当多个线程对同一个集合的内容进行操作时,

如何集成Perf4j到Java应用程序中并生成性能数据

在实际部署的生产环境能够以较低的风险及成本实现对业务逻辑级别性能问题的追踪.本文将介绍如何集成 Perf4j 到 Java 应用程序中并生成性能数据. 系统日志是应用程序问题诊断及运行维护的重要工具.Logback.Log4j 是常用于 Java 平台的日志记录 API. 目前大部分产品只是将系统重要参数.状态的变化及异常信息通过日志输出.本文将要介绍的 Perf4j 是一款专门用于 Java 服务器端代码计时.记录日志和监控结果的开源工具包.Perf4j 对常用日志工具包进行了扩展,能够将得到

聊聊Java和CPU的关系_java

其实写Java的人貌似和CPU没啥关系,最多最多和我们在前面提及到的如何将CPU跑满.如何设置线程数有点关系,但是那个算法只是一个参考,很多场景不同需要采取实际的手段来解决才可以:而且将CPU跑满后我们还会考虑如何让CPU不是那么满,呵呵,人类,就是这么XX,呵呵,好了,本文要说的是其他的一些东西,也许你在java的写代码时几乎不用关注CPU,因为满足业务才是第一重要的事情,如果你要做到框架级别,为框架提供很多共享数据缓存之类的东西,中间必然存在很多数据的征用问题,当然java提供了很多conc

java Web项目中什么场景中会用到java多线程?

问:能不能简单描述一下你在java web开发中需要用到多线程编程的场景? 对多线程有些了解,但是不太清楚具体的应用场景,能简单说一下你遇到的多线程编程的场景吗? 回答一: 最典型的如:1.用户注册完成送大礼包/积分之类,且积分等也是另一个系统并比较耗时:且这类任务即使失败也不是特别重要的.2.后台线程:比如定期执行一些特殊任务,如定期更新配置文件,任务调度(如quartz),一些监控用于定期信息采集等. 回答二: 最典型的应用比如tomcat,tomcat内部采用的就是多线程,上百个客户端访问