1.1 上下文切换: 多任务系统往往需要同时执行多道作业。 作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,为了让用户感觉这些任务正在同时进行, 操作系统的设计者巧妙地利用了时间片轮转的方式,CPU给每个任务都服务一定的时间, 然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。 任务的状态保存及再加载,这段过程就叫做上下文切换。 时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能, 但同时也带来了保存现场和加载现场的直接消耗。 (Note. 更精确地说, 上下文切换会带来直接和间接两种因素影响程序性能的消耗. 直接消耗包括: CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉; 间接消耗指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小). 真正干活的不是线程,而是CPU。线程越多,干活不一定越快。 但和处理器也支持多线程执行代码,是cpu 给每个线程分配 cpu时间片 来实现的. cpu时间片是 CPU分配给线程的时间,时间片 时间 非常短(几十毫秒). 所以 cpu 要不停的切换线程执行,感觉上是多个线程同时执行. CPU通过时间片分配算法来循环执行线程中的任务,当前线程的任务执行一个时间后会切换到下一个线程中的任务 ,但,在切换线程中的任务前 先保存上一个线程任务的状态,用于下一次切换回这个线程的任务时,可以再加载这个 线程的任务的状态. 所以:任务从保存到加载的过程 就是一次 上下文切换. 就像读书时,遇到不认识的字,停止阅读,保存下这本书第几页第几行的哪个字,然后找到字典查询,最后加载之前 保存的信息 找个这个字,认识后,继续阅读.切换是会影响效率的,同样上下文的切换也会影响多线程的执行速度 1.1.1 多线程一定快吗? 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() { 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); } } concurrency :1ms,b=-10000 serial:0ms,b=-10000,a=50000 ! 并发执行累加操作 不超过百万次 时 串行的执行速度要快,因为线程有创建和上下文切换的开销. 1.1.2 测试上下文切换次数和时长 Lmbench3是一个性能分析工具:测试上下文切换的时长 vmstat: 测量上下文切换的次数. $ vmstat 1 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 127876 398928 2297092 0 0 0 4 2 2 0 0 99 0 0 0 0 0 127868 398928 2297092 0 0 0 0 595 1171 0 1 99 0 0 0 0 0 127868 398928 2297092 0 0 0 0 590 1180 1 0 100 0 0 0 0 0 127868 398928 2297092 0 0 0 0 567 1135 0 1 99 0 0 可以看出:上下文1秒 切换1000多次 1.1.3如何减少上下文切换 无锁并发编程 哈希算法: 哈希算法并不是一个特定的算法而是一类算法的统称。 哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据, 经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。 如果是一个data数据集,经过哈希算法处理后得到key的数据集, 然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。 哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key, 那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。 稍微想一下就可以发现,既然输入数据不定长,而输出的哈希值却是固定长度的, 这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。 那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的, 所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。 密码上常用的MD5,SHA都是哈希算法,因为key的长度(相对大家的密码来说)较大所以碰撞空间较大, 有比较好的抗碰撞性,所以常常用作密码校验。 哈希(Hash)是一种数据编码方式,将大尺寸的数据(如一句话,一张图片,一段音乐、一个视频等) 浓缩到一个数字中,从而方便地实现数据匹配·查找的功能。 比如这里有一万首歌,要求按照某种方式保存好。到时候给你一首新的歌(命名为X), 要求你确认新的这首歌是否在那一万首歌之内。 无疑,将一万首歌一个一个比对非常慢。但如果存在一种方式, 能将一万首歌的每一首的数据浓缩到一个数字(称为哈希码)中, 于是得到一万个数字,那么用同样的算法计算新的歌X的编码, 看看歌X的编码是否在之前那一万个数字中,就能知道歌X是否在那一万首歌中。 将一首歌的5M字节数据浓缩到一个数字中的算法就是哈希算法。 那一万首歌按照各自的编码数字从小到大排序后得到的一个表就是哈希表。 显然,由于信息量的丢失,有可能多首歌的哈希码是同一个。 好的哈希算法会尽量减少这种冲突,让不同的歌有不同的哈希码。 最差的哈希算法自然就是所有的歌用那个算法算出来的都是同一个哈希码。 作为例子,如果要你组织那一万首歌, 一个简单的哈希算法就是让歌曲所占硬盘的字节数作为哈希码。 这样的话,你可以让一万首歌“按照大小排序”,然后遇到一首新的歌, 只要看看新的歌的字节数是否和已有的一万首歌中的某一首的字节数相同, 就知道新的歌是否在那一万首歌之内了。 对于一万首歌的规模而言,这个算法已经相当好,因为两首歌有完全相同的字节数是不大可能的。 就算真有极小概率出现不同的歌有相同的哈希码,那也只有寥寥几首歌,此时再逐首比对即可。 多线程竞争锁时,引起上下文切换.避免用锁:将数据的id按照hash算法 取模分段 不同的线程处理不同段的数据. cas算法 Atomic包使用cas算法来更新数据.而不需要加锁 使用最少线程 任务少,创建出多个线程,会造成大量线程处于等待状态. 使用协程 单线程中实现多任务的调度,并在单线程里维持多个任务间的切换. 1.1.4减少上下文切换实战 减少等待线程 减少上下文切换 第一步:用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么 sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17 第二步:统计所有线程分别处于什么状态,发现300多个线程处于WAITING(onobjectmonitor)状态。 [tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c 39 RUNNABLE 21 TIMED_WAITING(onobjectmonitor) 6 TIMED_WAITING(parking) 51 TIMED_WAITING(sleeping) 305 WAITING(onobjectmonitor) 3 WAITING(parking) 第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。发现这些线 程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线 程都闲着。 "http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object.wait() [0x0000000052423000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker) at java.lang.Object.wait(Object.java:485) at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464) - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker) at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489) at java.lang.Thread.run(Thread.j 第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100。 <maxThreads="250" maxHttpHeaderSize="8192" emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75" maxPostSize="512000" protocol="HTTP/1.1" enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384" connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true"> 第五步:重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程,发现 减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从 WAITTING到RUNNABLE都会进行一次上下文的切换。读者也可以使用vmstat命令测试一下。 [tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c 44 RUNNABLE 22 TIMED_WAITING(onobjectmonitor) 9 TIMED_WAITING(parking) 36 TIMED_WAITING(sleeping) 130 WAITING(onobjectmonitor) 1 WAITING(parking)
1.1 上下文切换
最新推荐文章于 2021-06-05 23:05:11 发布