性能和可伸缩性

对性能的思考

I.性能与可伸缩性

衡量程序的性能:

“运行速度”-服务时间、等待时间

“处理能力”-生产量、吞吐量

可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加。

当进行性能调优时,其目的通常是用更小的代价完成相同的工作;在进行可伸缩性调优时,其目的是设法将问题的计算并行化,从而能利用更多的计算资源来完成更多的工作。

II.评估各种性能权衡因素

避免不成熟的优化。首先使程序正确,然后再提高运行速度。

 

Amdahl定律

在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占的比重(显然越少越好)。  

 

线程引入的开销

I.上下文切换

切换上下文需要一定的开销,而在线程调度过程中需要访问由操作系统和JVM共享的数据结构。vmstat(UNIX),perfmon(Windows)

II.内存同步

竞争性同步带来的开销

III.阻塞

竞争的同步可能需要操作系统的介入,从而增加开销。JVM在实现阻塞行为时,可以采用自旋等待(Spin Waiting,指通过循环不断地尝试获取锁,直到成功)或通过操作系统挂起被阻塞的线程。

 

减少锁的竞争

串行操作会降低可伸缩性,并且上下文操作也会降低性能。在锁上发生竞争时将同时导致这两种问题,因此减少锁的竞争能够提高性能和可伸缩性。

I.缩小锁的范围(“快进快出”)

降低发生竞争可能性的一种有效方式就是尽可能缩短锁的持有时间。例如,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作(I/O)。同步需要一定的开销,当把一个同步代码块分解为多个同步代码块时,反而会对提升性能产生负面影响。

示例代码:

优化前

public class AttributeStore {
    private final Map<String, String> attributes = new HashMap<>();

    public synchronized boolean userLocationMatches(String name, String regexp) {
        String key = "user." + name + ".location";
        String location = attributes.get(key);
        if (null == location)
            return false;
        else
            return Pattern.matches(regexp, location);
    }
}

优化后

public class BetterAttributeStore {
    private final Map<String, String> attributes = new HashMap<>();

    public boolean userLocationMatches(String name, String regexp) {
        String key = "user." + name + ".location";
        String location;
        synchronized(this){
            location = attributes.get(key);
        }
        if (null == location)
            return false;
        else
            return Pattern.matches(regexp, location);
    }
}

II.减小锁的粒度(锁分解)

如果一个锁需要保护多个互相独立的状态变量,那么可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而提高可伸缩性,最终降低每个锁被请求的频率。

示例代码:

优化前

public class ServerStatus {
    public final Set<String> users;
    public final Set<String> queries;
	//......
    public synchronized void addUser(String u) {
        users.add(u);
    }
    public synchronized void removeUser(String u) {
        users.remove(u);
    }
    public synchronized void addQuery(String u) {
        queries.add(u);
    }
    public synchronized void removeQuery(String u) {
        queries.remove(u);
    }
}

优化后

public class BetterServerStatus {
    public final Set<String> users;
    public final Set<String> queries;
	//......
    public void addUser(String u) {
        synchronized (users) {
            users.add(u);
        }
    }
    public void removeUser(String u) {
        synchronized (users) {
            users.remove(u);
        }
    }
    public void addQuery(String u) {
        synchronized (queries) {
            queries.add(u);
        }
    }
    public void removeQuery(String u) {
        synchronized (queries) {
            queries.remove(u);
        }
    }
}

III.锁分段

在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列通的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。劣势:实现独占访问更难且开销更高。

示例代码:

public class StripedMap {
    //同步策略:buckets[n]由locks[n%N_LOCKS]来保护
    private static final int N_LOCKS = 16;
    private final Node[] buckets;
    private final Object[] locks;

    public StripedMap(int numBuckets) {
        buckets = new Node[numBuckets];
        locks = new Object[N_LOCKS];
        for (int i = 0; i < N_LOCKS; i++) {
            locks[i] = new Object();
        }
    }

    private final int hash(Object key) {
        return Math.abs(key.hashCode() % buckets.length);
    }

    public Object get(Object key) {
        int hash = hash(key);
        synchronized (locks[hash % N_LOCKS]) {
            for (Node m = buckets[hash]; m != null; m = m.next)
                if (m.key.equals(key))
                    return m.value;
        }
        return null;
    }

    public void clear() {
        int hash;
        for (int i = 0; i < buckets.length; i++) {
            hash = hash(buckets[i].key);
            synchronized (locks[hash % N_LOCKS]) {
                buckets[i] = null;
            }
        }
    }

    private static class Node {
        public Node next;//...
        public Object key;
        public Object value;
    }

}

IV.避免热点域

当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能与可伸缩性之间互相制衡的另一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些“热点域(Hot Field)”,而这些热点域往往会限制可伸缩性。例如HashMap中的计数器。为了避免热点域,ConcurrentHashMap为每个分段都维护了一个独立的计数器,并通过每个分段的锁来维护这个值,size将对每个分段进行枚举并将每个分段中的元素数量相加(每当调用size时,将返回值缓存到一个volatile变量中,并且每当容器被修改时,使这个缓存中的值无效(将其设为-1),如果发现缓存的值非负,那么表示这个值是正确的,可以直接返回,否则重新计算)。

V.替代独占锁

使用并发容器、读-写锁、不可变对象以及原子变量。

减少上下文切换的开销

许多任务中都包含一些可能被阻塞的操作。当任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换。在服务器应用程序中,发生阻塞的原因之一就是在处理请求时产生各种日志消息。通过将I/O操作从处理请求的线程中分离出来,可以缩短处理请求的平均服务时间。调用log方法的线程将不会再因为等待输出流的锁或者I/O完成而被阻塞,它们只需将消息放入队列,然后就返回到各自的任务中。

参考:《Java并发编程实战》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值