博客网站开发技术,中小企业网站建设行情,网络广告平台有哪些,手机网站编辑java并发编程
线程堆栈大小
单线程的堆栈大小默认为1M#xff0c;1000个线程内存就占了1G。所以#xff0c;受制于内存上限#xff0c;单纯依靠多线程难以支持大量任务并发。
上下文切换开销
ReentrantLock
2个线程交替自增一个共享变量#xff0c;使用ReentrantLock1000个线程内存就占了1G。所以受制于内存上限单纯依靠多线程难以支持大量任务并发。
上下文切换开销
ReentrantLock
2个线程交替自增一个共享变量使用ReentrantLock每个线程1000w次这是vmstat的结果
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 20476 886508 207672 2901024 0 0 0 0 583 1128 0 0 100 0 0 2 0 20476 857280 207672 2901060 0 0 0 164 2612 4980 13 3 83 0 0 1 0 20476 832052 207672 2901060 0 0 0 0 7038 21799 40 2 57 0 0 3 0 20476 830336 207672 2901060 0 0 0 0 5591 14159 41 2 57 0 0 0 0 20476 887988 207672 2901060 0 0 0 0 5170 13119 28 2 70 0 0 1 0 20476 888068 207672 2901028 0 0 0 0 560 1117 0 0 100 0 0
vmstat输出参数参看 https://www.cnblogs.com/ggjucheng/archive/2012/01/05/2312625.html
我们注意到cs上下文切换达到过21799的峰值相应的in中断次数、us用户cpu时间也随之上升整体耗时在2.7s。 究其原因锁的争用会触发系统调用迫使线程进入沉睡系统调用又增加了用户态和内核态的上下文切换次数。
CAS
2个线程交替自增一个共享变量使用CAS每个线程1000w次这是vmstat的结果 procs -----------memory---------- —swap-- -----io---- -system-- -----cpu------ r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 20476 879772 207672 2901068 0 0 0 0 873 1532 2 3 95 0 0 0 0 20476 887484 207672 2901076 0 0 0 0 2559 3206 30 3 67 0 0 0 0 20476 887484 207672 2901076 0 0 0 0 587 1065 1 0 99 0 0
cs峰值只到3206整体耗时在400ms左右。 由于CAS是用户态操作不涉及上下文切换所以cs次数较少我们认为这里的数值仅仅是线程正常切换导致。
无锁
单线程自增2000w次 r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 20476 878564 207676 2901108 0 0 0 0 733 1228 1 1 98 0 0 0 0 20476 886216 207676 2901108 0 0 0 0 2453 3443 11 3 86 0 0 0 0 20476 886216 207676 2901104 0 0 0 0 662 1171 0 0 99 0 0 非常快几个毫秒跑完。本次cs与CAS下的cs差不多印证了3000多次的cs只是正常的操作系统线程调度。然后我们会看到CAS下的us值为30明显高于单线程值为11。这是因为CAS的自增行为本质上是一个循环CAS不会释放cpu这是AtomicInteger自增的源码
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 var4));return var5;}我们看到getAndAddInt会反复尝试直到自增成功为止。代码里的compareAndSwapInt就是CAS操作。
synchronized
r b swpd free buff cache si so bi bo in cs us sy id wa st 3 0 20476 885204 206452 2869528 0 0 0 0 2336 3461 14 5 81 0 0 2 0 20476 884668 206452 2869548 0 0 0 0 7332 19534 40 4 55 0 0 0 0 20476 911968 206452 2869520 0 0 0 0 3608 7762 20 3 77 0 0 0 0 20476 911968 206452 2869520 0 0 0 0 693 1290 0 0 100 0 0
耗时1.7scs的峰值高于CAS但要低于ReentrantLock。具体原因我估计是因为jvm1.6之后对synchronized做过优化的缘故synchronized并不会一开始就用lock那样的重量级锁而是按照“偏向锁–自旋锁–重量级锁”的顺序来逐步升级的前两者都是用户态的指令并不触发cs。但由于竞争的存在重量级锁又不可能完全避免所以synchronized下的cs要低于ReentrantLock但又明显高于完全用户态的CAS。
总结
1、java并发编程下锁的推荐使用顺序越前者越推荐 无锁 -- CAS -- synchronized -- ReentrantLock 2、上下文切换的耗时是用户态CAS指令的6~7倍应尽量避免。
延伸讨论
对于IO密集型应用如果无法做到“无锁编程”最佳的并发编程模型应该是协程而非使用多线程。我们以go语言来说明。
go语言
go的设计原则是避免一切阻塞。 如果一个goroutine将要陷入系统调用go调度器立刻从当前线程分离它转而执行其他goroutine。这一点跟python的greenlet是类似的处理。 举个例子goroutine A在等待channel的消息阻塞的只是A而不是执行A的线程TT会在A被阻塞的这段时间被调度去执行goroutine B。 另外这里的系统调用我理解不仅仅是IO由于锁争用导致的线程挂起也是系统调用同样会导致goroutine的切换。总之记住一点线程不会阻塞阻塞的是goroutine。
volatile
volatile也是java里并发编程的手段之一。前面的例子之所以没有提到是因为volatile不能保证自增的并发正确性自增操作依赖于原值其实是一个复合操作。
首先java字节码层面没法看出volatile与普通变量有何区别比如下面代码
private static volatile int race_ 0;
public static void main(String[] args)
{race_;
}翻译成java字节码是
0: getstatic #2 // Field race_:I
3: iconst_1
4: iadd
5: putstatic #2 // Field race_:I看起来就是操作一个普通的static变量嘛。
我们只能从JIT的反汇编才能看出一些端倪
0x000000000257ce9e: mov rsi,0d59c01b0h ; {oop(a java/lang/Class com/lee/MainFlow)},获得类的地址,race_在类地址的偏移为0x88处0x000000000257cea8: mov edi,dword ptr [rsi88h] ;*getstatic race_; - com.lee.MainFlow::myincr0 (line 59)0x000000000257ceae: inc edi0x000000000257ceb0: mov dword ptr [rsi88h],edi0x000000000257ceb6: lock add dword ptr [rsp],0h ;*putstatic race_; - com.lee.MainFlow::myincr5 (line 59)race的地址是rsi88hdword ptr [rsi88h]表示取得race_的内存值通过 mov edi,dword ptr [rsi88h] 将race的内存值赋给edi寄存器接着通过 inc edi 实现自增最后将自增的结果通过 mov dword ptr [rsi88h],edi 返回到内存。
由于race_是int型所以自增操作在32位寄存器edi里就可以完成了无需使用rdi。
注意最后一条汇编指令 lock add dword ptr [rsp],0h 该指令在race为非volatile类型下是没有的即非volatile版本执行完 mov dword ptr [rsi88h],edi 对内存的重新赋值就会返回了。
add dword ptr [rsp],0h指令把栈顶值加0这是什么鬼其实add是一个无意义的占位操作只是由于lock后面必须跟特定的指令例如ADD、XCHG等MOV指令不能跟在lock后所以才这么写。lock会锁内存总线保证将cpu高速缓存L1/L2里当前缓存行的数据刷新到主存同时使得其他cpu的高速缓存失效。lock之前的那条指令 mov dword ptr [rsi88h],edi 看似将寄存器的结果放到了内存但由于硬件操作的异步性有可能只是放到了cpu高速缓存里而并未真正写到内存。一般来说cpu对内存的写分为两种write-through和write-back前者同时写内存和高速缓存后者只写高速缓存写内存则被推迟到随后的某个时机。像linux操作系统使用的就是write-back所以linux下的内存赋值不是立即生效的。
我们写一段伪码来表示就更容易理解了
inc edi
mov dword ptr [rsi88h],edi
flush由上可见volatile关键字的几个特点 原子性 多线程间可见性。
这两个特点就来自于机器指令中的lock前缀这里仅考虑多核情况单核是无需lock前缀的反正也没人跟你抢lock会锁总线禁止其他cpu对内存的访问原子性同时可能导致其他cpu缓存的失效触发重读多线程间可见性。
还有一点需要特别指出虽然volatile可以保证原子性但反过来指令的原子性并不是一定得靠volatile保证例如java虚拟机规范就规定了除long和double外的基本类型的读写都是原子的引用的读写也是原子的见https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7这些都无需volatile来保证其原子性在这些基本类型上使用volatile仅仅利用的是volatile的“多线程间可见性”例如bool型变量的多线程感知或者“禁止指令重排序”作用例如double-check。
附录
lock前缀简介
LOCK前缀导致处理器在执行指令时会置上LOCK#信号于是该指令就被作为一个原子指令atomic instruction执行。在多处理器环境下,置上LOCK#信号可以确保任何一个处理器能独占使用任何共享内存。
注意:后来的Intel64和IA32处理器(包括Pentium4,Intel Xeon, P6)有时即使没有置上LOCK#信号也会产生锁动作的。
LOCK前缀只能放在下列指令前面: ADD, ADC, AND, BTC,BTR,BTS,CMPXCHG, CMPXCH8B, DEC,INC, NEG,NOT, OR, SBB, SUB, XOR, XADD以及XCHG。如果LOCK指令用在了非上述指令前则会引发#UD异常(undefined opcode exception未定义操作数异常)而且LCOK前缀的指令的目标操作数只能是内存寻址方式否则也会引发#UD异常的.XCHG指令不管前面有无LOCK前缀都会置上LOCK#信号即XCHG总是作为原子指令执行。
LOCK前缀常常放在BTS前用来实现对一个共享内存的读-修改-写(read-modify-write)原子化操作。
内存是否地址对齐并不影响LOCK前缀的功能。实际上内存锁定对任何非对齐内存地址都起作用的。
这个指令的操作在64位和非64位模式下是一致的。
vmstat关键输出参数说明
cs 每秒上下文切换次数例如我们调用系统函数就要进行上下文切换线程的切换也要进程上下文切换这个值要越小越好太大了要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中我们一般做性能测试时会进行几千并发甚至几万并发的测试选择web服务器的进程可以由进程或者线程的峰值一直下调压测直到cs到一个比较小的值这个进程和线程数就是比较合适的值了。系统调用也是每次调用系统函数我们的代码就会进入内核空间导致上下文切换这个是很耗资源也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换导致CPU干正经事的时间少了CPU没有充分利用是不可取的。
in 每秒CPU的中断次数包括时间中断
us 用户CPU时间我曾经在一个做加密解密很频繁的服务器上可以看到us接近100,r运行队列达到80(机器在做压力测试性能表现不佳)。
id 空闲 CPU时间一般来说id us sy 100,一般我认为id是空闲CPU使用率us是用户CPU使用率sy是系统CPU使用率。