励精图治
线程存在的价值
线程自然是为了提高运行效率, 但是更重要的是 更好的用户体验. 用户是有洞察一个任务执行的速度的能力. 体验更好就是更快.
所以,线程是为了更好的用户体验. 这包含更快,更及时,更充分.
线程解决的任务
往往线程所解决的任务是因为资源紧缺.
包括 CPU,内存,IO带宽,网络带宽,磁盘空间等等。
对症下药
一个任务是否需要多线程,这取决的这个任务本身,以及运行的环境。并非所有的情况都适用多线程,有时候最优的线程数量是1也不一定。
举例来说,如果是一个计算密集型,而计算环境,是单CPU。那么,其实最优的线程数量就是1.再多的线程也不会带来性能的提高。
当然,如果你的CPU是4核8线程,理论上自然就是8线程是最优了。一般还会+1=9, 这是为了防止出现问题之后,有一个线程能及时的接替。保持CPU的繁忙。
可伸缩性的定义
当计算资源增加时,性能,吞吐量或者处理能力也能跟着增加。
一般来说,性能就是快,多。吞吐量跟运行速度。但是这两者是独立的。甚至是相互抑制的。
对服务器来说,吞吐量才是关注的重点,对客户端来说,是吞吐量还是处理速度就不一定了。
评论环境对性能的影响
要从几个方面去看
1. 想要怎样的?更快?更多处理量?更高响应?
2. 在什么环境下用会更快?是低负荷还是高负荷?是大数据还是小数据?
3. 能验证吗?
4. 能复用吗?修改的代码还有其他地方可以直接套用吗?
5. 代价是什么?是内存还是空间?还是CPU?我们能不能接受这个代价?
注意事项:
1. 并发环境下问题比串行环境下的问题,更难查。
2. 并发会有更多的安全隐患,需要更加细致
3. 需求一定要明确!!!必须要弄清楚
4. 能够在接近或者完全真实的使用环境下,测试才是验证优化是否成功的唯一标准
Amdahl定律(阿姆达尔定律)
是否资源的无限提供就能得到正比例的回报?答案是否定的。这取决于,程序中穿行部分 跟 并行部分的比例。任何一个程序都是有串行部分的。
详细的看这个: .htm
这幅图表明CPU的数量越多,在不同的串行比例下,所带来的性能提升。
并行的比例越大,其带来的性能提升幅度就越高。
来一段简介的代码
BlockingQueue<Runnable> queue;queue = ......;
while (true) {
try{
Runnable runable = queue.take();//#这里就是串行部分
runnable.run();
}catch(InterruptedException e) {
}
}
读取runnable是串行的,runnable.run可以是并行的
日志线程,对文件写入log,是串行的,可以有多个线程会有写入的操作。大概就是这么个意思。
Amadhl给出了更多的性能提升的极限。这给出来性能提升的范畴。
在评估一个算法是否具备良好的可伸缩性,就要考虑在数百个CPU的情况的性能表现。
这里就涉及到,两个技术,锁分段,锁分解,其实就是一回事,前者把一个锁分成两个,后者分成多个。无差别嘛。多个名字也不知道做什么。(这个等下说)
多线程的性能消耗
1. context消耗,中文名 上下文消耗
怎么理解呢?
我们知道主线程只有一个,若 可运行线程 > CPU的数量,那 就存在 线程被调用出去的情况,每调出去一次,就需要切换一次 context。
这可能会包含 缓存资源的切换,你需要的可能不在里头,你已经缓存的可能又被调出去、Cpu时钟周期的消耗等等--------------(有朋友有补充的吗,欢迎留言)
2.内存同步开销
常见的关键字就是synchronized volatile,扯远点说,memory里头有个memory barrier(内存栅栏)。在这里头的东西,不能被排序,也会抑制编译器优化。
同步分为内存竞争的同步,内存不竞争的同步。
synchronized对内存无竞争的有优化。这个呢就有你来我去这么个顺序,竞争是妥妥的存在的。---------这里的同步一般也都是串行的
volatile一般都是无竞争的。基本上影响微乎其微。可以忽略不计。所以说volatile是常驻主存的。
对于非竞争的同步,我们基本上可以无视这些影响。。。不用过分深究。没有任何意义。
非要说,就可以理解成JVM本身会有优化,比如将相近相同类型操作用一个锁合并起来。----锁粗粒度化
注意一下,一个线程中的同步可能会影响其他线程,怎么说呢?同步会带来共享内存总线上的消耗,总线的带宽是有限的,并且所有的线程都共享这条带宽。
可想而知,如果一个线程占用了共享内存总线太多的资源,必然会对其他线程产生影响。
3.等待,阻塞----不必深究,JVM相关的,了解就行
一般来说,CPU是轮询,如果一个同步这个CPU周期或得不到,下个周期可能会被继续尝试。那么这里JVM可能会采取挂起,或者轮询的方式,
这是JVM的自选。我们也左右不了。了解就好
减少锁竞争
之前说的锁分段、锁分解就是这里用的。
对可伸缩性最大的影响就是锁。一个锁要同步起来就麻烦了。其他的都要等。所以,要减少锁竞争!
方式有3
1. 减少时间-锁持有时间减少----就是减小锁住的代码块,以及代码块里头的运行时间
2. 降低请求频率--减少锁的使用-----就是只在必要的地方用锁
3. 优化锁-----利用锁分段锁分解的技术,提高锁的并发性
减小锁同步时间范例
1.Map<String, String> map = new HashMap<String, String>();
public synchronized String doSomething(String key){
String value = map.get(key);
if (value == null){
return "false";
} else {
return value;
}
}
改一改
Map<String, String> map = new HashMap<String, String>();
public String doSomething(String key){//#去掉synchronized
String value;
synchronized(this){//#改到这里,这样代码块就小了
value = map.get(key);
}
if (value == null){
return "false";
} else {
return value;
}
}
这段代码里头可能效果不大,就那么个意思
范例2 减小锁的请求频率-------同一个操作依旧需要锁,怎么减小频率,那就是 降低锁的粒度。3件事情1个锁,不如3件事情3个锁。
public class MyClass {
Map<String> Mp1 = new Map<String>();
Map<String> Mp2 = new Map<String>();
public synchronized void addMp1(String v){
Mp1.add(v);
}
public synchronized void addMp2(String v){
Mp2.add(v);
}
public synchronized void removeMp1(String v){
Mp1.remove(v);
}
public synchronized void removeMp2(String v){
Mp2.remove(v);
}
}
这里的Mp1跟Mp2没什么关系。
锁粒度分开怎么做呢
public class MyClass {
Map<String> Mp1 = new Map<String>();
Map<String> Mp2 = new Map<String>();
public void addMp1(String v){
synchronized(Mp1) Mp1.add(v);
}
public void addMp2(String v){
synchronized(Mp2) Mp2.add(v);
}
public void removeMp1(String v){
synchronized(Mp1) Mp1.remove(v);
}
public void removeMp2(String v){
synchronized(Mp2) Mp2.remove(v);
}
}
这样,锁粒度就降低了
再来一段从ConcurrentHashMap.java中弄出来的锁粒度细化
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//#计算hashcode值。
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//#这里的n赋值
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//#关键在这里i=(n -1) & hash, 这里计算得到了需要的Node<K,V> f,之后对这个f加锁
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
这里的f也就一种锁粒度细化,不继续深究了。有兴趣的话,可以自己找下这个文件android中是在 libcore/luni/src/main/java/java/util/concurrent/ConcurrentHashMap.java这里
以上的锁分解,锁分段在 线程请求同一个变量中就很好操作了
那么如果是一个锁请求多个变量呢
锁热点域
有部分的锁会经常被调用,怎么办呢?
利用锁分解的原理,将这个热点域也分解掉。由多个区块分开计算。
比如范例2中的加减,我要统计总操作次数,怎么做呢?
正规做法范例
public class MyClass {
Map<String> Mp1 = new Map<String>();
Map<String> Mp2 = new Map<String>();
private int count = 0;//#总的剩下的Map中的数量
public void addMp1(String v){
synchronized(Mp1) Mp1.add(v);
synchronized(count) count++;
}
public void addMp2(String v){
synchronized(Mp2) Mp2.add(v);
synchronized(count) count++;
}
public void removeMp1(String v){
synchronized(Mp1) Mp1.remove(v);
synchronized(count) count--;
}
public void removeMp2(String v){
synchronized(Mp2) Mp2.remove(v);
synchronized(count) count--;
}
public int getCount(){
synchronized(count) return count;
}
}
改进一下
public class MyClass {
Map<String> Mp1 = new Map<String>();
Map<String> Mp2 = new Map<String>();
private int count = 0;//#总的剩下的Map中的数量
private int countMp1 = 0;//#Mp1的Map中的数量
private int countMp2 = 0;//#Mp2的Map中的数量
public void addMp1(String v){
synchronized(Mp1) Mp1.add(v);
synchronized(countMp1) countMp1++;
}
public void addMp2(String v){
synchronized(Mp2) Mp2.add(v);
synchronized(countMp2) countMp2++;
}
public void removeMp1(String v){
synchronized(Mp1) Mp1.remove(v);
synchronized(countMp1) countMp1--;
}
public void removeMp2(String v){
synchronized(Mp2) Mp2.remove(v);
synchronized(countMp2) countMp2--;
}
public int getCount(){
return countMp1 + countMp2;//#这里有问题吗?好像没有。有发现问题的同学记得留言。被并发弄的神神叨叨的了
}
}
之前的热点是count。这样分一开,热点就分散了。大概是这么个意思。
还有一些代替独占锁的方法
1. AtomicInteger之类的原子操作
2. ReadWriteLock:多个读取,一个写。并发也更高
检测CPU的利用率,分析原因
分几种情况
1. 负载不足---------------想办法增加负载
2. IO密集---------------看下能不能提高带宽
3. 外部资源条件限制---------------跟我没关系。都是别人的事
4. 锁竞争---------------优化优化
减少对象池的使用
对象 池 很坑爹。线程从对象池取对象,对象池里头存在同步,你懂的。同步!!!
当然对象的分配比同步开销肯定要少的。
减少context消耗
怎么减少呢?比如说log日志。
后台起了几个线程去写入。不要等待锁获取,直接交给一个队列,让队列去操作去吧。再多的等待也跟线程没关系了。只是提交的话,还是很快的。
小结
要弄好多线程,要问自己,目的是为什么,什么环境,怎么做,效果如何,能不能验证,什么代价。
要注意性能提高是有限度的。其实串行跟并行的比例很难确定。一般也都是毛估估的。
串行=独占,串行越少,并行的方式在资源增加的时候提供性能的比例就越大。
少用锁,用的代码块减少,用非独占锁和非阻塞锁代替独占锁。
更多推荐
励精图治
发布评论