励精图治---Concurrency---为什么要有线程

来源:互联网 发布:sql update 效率 编辑:程序博客网 时间:2024/05/18 01:19

线程存在的价值

线程自然是为了提高运行效率, 但是更重要的是  更好的用户体验. 用户是有洞察一个任务执行的速度的能力. 体验更好就是更快.

所以,线程是为了更好的用户体验. 这包含更快,更及时,更充分.


线程解决的任务

往往线程所解决的任务是因为资源紧缺.

包括 CPU,内存,IO带宽,网络带宽,磁盘空间等等。


对症下药

一个任务是否需要多线程,这取决的这个任务本身,以及运行的环境。并非所有的情况都适用多线程,有时候最优的线程数量是1也不一定。

举例来说,如果是一个计算密集型,而计算环境,是单CPU。那么,其实最优的线程数量就是1.再多的线程也不会带来性能的提高。

当然,如果你的CPU是4核8线程,理论上自然就是8线程是最优了。一般还会+1=9, 这是为了防止出现问题之后,有一个线程能及时的接替。保持CPU的繁忙。


可伸缩性的定义

当计算资源增加时,性能,吞吐量或者处理能力也能跟着增加。


一般来说,性能就是快,多。吞吐量跟运行速度。但是这两者是独立的。甚至是相互抑制的。

对服务器来说,吞吐量才是关注的重点,对客户端来说,是吞吐量还是处理速度就不一定了。 


评论环境对性能的影响

要从几个方面去看

1. 想要怎样的?更快?更多处理量?更高响应?

2. 在什么环境下用会更快?是低负荷还是高负荷?是大数据还是小数据?

3. 能验证吗?

4. 能复用吗?修改的代码还有其他地方可以直接套用吗?

5. 代价是什么?是内存还是空间?还是CPU?我们能不能接受这个代价?


注意事项:

1. 并发环境下问题比串行环境下的问题,更难查。

2. 并发会有更多的安全隐患,需要更加细致

3. 需求一定要明确!!!必须要弄清楚

4. 能够在接近或者完全真实的使用环境下,测试才是验证优化是否成功的唯一标准


Amdahl定律(阿姆达尔定律)

是否资源的无限提供就能得到正比例的回报?答案是否定的。这取决于,程序中穿行部分  跟 并行部分的比例。任何一个程序都是有串行部分的。

详细的看这个: http://baike.baidu.com/view/1349114.htm


对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了如下公式:
S=1/(1-a+a/n)
其中,a为并行计算部分所占比例,n为并行处理结点个数。这样,当1-a=0时,(即没有串行,只有并行)最大加速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。这一公式已被学术界所接受,并被称做“阿姆达尔定律”,也称为“安达尔定理”(Amdahl law)。

这幅图表明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日志。

后台起了几个线程去写入。不要等待锁获取,直接交给一个队列,让队列去操作去吧。再多的等待也跟线程没关系了。只是提交的话,还是很快的。


小结

要弄好多线程,要问自己,目的是为什么,什么环境,怎么做,效果如何,能不能验证,什么代价。

要注意性能提高是有限度的。其实串行跟并行的比例很难确定。一般也都是毛估估的。

串行=独占,串行越少,并行的方式在资源增加的时候提供性能的比例就越大。

少用锁,用的代码块减少,用非独占锁和非阻塞锁代替独占锁。

0 0
原创粉丝点击