性能与可伸缩性——《java并发编程实战》
概述
线程的最主要目的是提高程序的运行性能。线程可以使程序更加充分地发挥系统的可用处理能力,从而提高系统的资源利用了吧。此外,线程可以使程序在运行现有任务的情况下立即处理新的任务,从而提高系统的响应性。
对性能的思考
提升性能意味着用更少的资源做更多的事情。“资源”的含义很广。对于一个给定的操作,通常会缺乏某种特定的资源,例如CPU时钟周期、内存、网络带宽、I/O带宽、数据库请求、磁盘空间以及其他资源。当操作性能由于某种特定的资源而受到限制时,我们通常将该操作称为资源密集型操作,例如CPU密集型、数据库密集型等。
使用多个线程总会引发一些额外的性能开销。造成这些开销的操作包括:线程之间的协调(如加锁、触发信号、内存同步等),增加的上下文切换,线程的创建和销毁,以及线程的调度等。
想要通过并发来获得更好的性能,需要努力做好两件事情:有效地利用现有处理资源以及在出现新的处理资源时使程序尽可能地利用这些资源。
1)性能的可伸缩性
可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加
2)评估各种性能权衡因素
避免不成熟的优化。首先使程序正确,然后再提高运行速度——如果它还运行得不够快
以测试为基准,不要猜测
Amadhl定律
Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现更高加速比,这个值取决于程序中可并行组件与串行组件所占的比重。
在所有并发程序中都包含一些串行部分。
1)在各种框架中隐藏的串行部分
2)Amdahl定律的应用
线程引入的开销
程引入的开销
在多个线程的调度和协调过程都需要一定的性能开销:为了提升性能而引入的线程来说,并行带来的性能提升必须超过并发导致的开销
1)上下文切换
2)内存同步
3)阻塞
减少锁的竞争
在并发程序上,对可伸缩性的最大威胁就是独占方式的资源锁
有三种方式可以降低锁的竞争程度:
减少锁的持有时间
降低锁的请求频率
使用带有协调机制的独占锁,这些机制允许更高的并发性
1)、缩小锁的范围(“快进快出”)
2)、减小锁的粒度
3)、锁分段
在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。
锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。
4)、避免热点域
当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能和可伸缩性之间相互制衡的另一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些"热点域",而这些热点域往往会限制可伸缩性。
在实现HashMap时,你需要考虑如何在size方法中计算Map中的元素数量发。最简单的方法就是,在每次调用时都统计一次元素的数量。一种常见的优化措施是,在插入和移除元素时更新一个计数器,虽然这在put和remove方法中稍微增加了一些开销,以确保计数器是最新的值,但这将把size的开销从O(n)降低到O(1).在单线程或者完全采用同步的实现中,使用一个独立的计数能很好地提高类似size和isEmpty这些方法的执行速度,但却导致更难以提升实现的可伸缩性,因为每个修改map的操作都需要更新这个共享的计数器。即使使用锁分段技术来实现散列链,那么在计数器的访问进行同步时,也会重新导致在使用独占锁时存在的可伸缩性问题。一个看似性能优化的措施——缓存size操作的结果,已经变成了一个可伸缩性问题。在这种情况下,计数器也被称为热点域,因为每个导致元素数量发生变化的操作都需要访问它。
为了避免这个问题,ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段的元素数量相加,而不是维护一个全局计数。为了避免枚举每个元素,ConcurrentHashMap为每个分段都维护了一个独立的计数,并通过每个分段的锁来维护这个值。
5)、一些替代独占锁的方法
第三种降低竞争锁的影响的计数就是放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如,使用并发容器、读-写锁、不可变对象以及原子变量
6)、检测CPU的利用率
如果所有CPU的利用率并不均匀,那么你的首要目标是进一步找出程序中的并行性。如果CPU没有得到充分利用,那么需要找出其中的原因。通常有以下几种原因:
负载不充足
测试时增加负载
I/O密集
可通过iostat或perfmon来判断某个应用程序是否是磁盘I/O密集型的,或者通过检测网络的通信流量级别来判断它是否需要高带宽
外部限制
锁竞争
7)、向线程池说不
在JVM的早期版本中,对象分配和垃圾回收等操作的执行速度慢,后续的版本中,这些操作的性能得到了极大的提高。
为了解决“缓慢的”对象生命周期问题,许多开发人员都喜欢使用对象池技术,在对象池中,对象能被循环利用,而不是由垃圾回收器回收并在需要时分配。
通常,对象分配操作的开销比同步的开销更低
减少上下文切换的开销
在许多任务中都包含一些可能被阻塞的操作。当任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换
把可能I/O操作从处理请求线程转移到一个专门的线程,类似于两种不同救火方案之间的差异:第一种方案是所有人排成一队,通过传递水桶来救火;第二种方案是每个人都拿着一个水桶去救火。在第二种方案张,每个人都可能在水源和着火点上存在更大的竞争,此外救火的效率也更低,因为每个人都在不停的切换模式。在第一种解决方案中,睡不断地从水源传递到燃烧的建筑物,人们付出更少的体力却传递了更多的水,并且每个人从头至尾只需做一项工作。正如中断会感染人们的工作并降低效率,阻塞和上下文切换同样会干扰线程的正常执行。
package chapter10; import java.util.concurrent.BlockingQueue; /** * @author zhen * @Date 2018/11/23 10:23 * 对任务队列的串行访问 */ public class WorkerThread extends Thread { private final BlockingQueue<Runnable> queue; public WorkerThread(BlockingQueue<Runnable> queue) { this.queue = queue; } public void run() { while(true) { try{ Runnable task = queue.take(); }catch (InterruptedException e) { break; /* 允许线程退出 */ } } } }
package chapter10; import common.ThreadSafe; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; /** * @author zhen * @Date 2018/11/23 10:40 * 将一个锁不必要地持有过长时间 */ @ThreadSafe public class AttributeStore { private final Map<String, String> attributes = new HashMap<String, String>(); public synchronized boolean userLocationMatches(String name, String regexp) { String key = "user." + name + ".location"; String location = attributes.get(key); if (location == null) { return false; } else { return Pattern.matches(regexp, location); } } }
package chapter10; import common.ThreadSafe; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; /** * @author zhen * @Date 2018/11/23 10:43 * 减少锁持有的时间 */ @ThreadSafe public class BetterAttributeStore { private final Map<String, String> attributes = new HashMap<String, String>(); public boolean userLocationMatches(String name, String regexp) { String key = "user." + name + ".location"; String location; synchronized (this) { location = attributes.get(key); } if (location == null) { return false; } else { return Pattern.matches(regexp, location); } } }
package chapter10; import common.GuardedBy; import common.ThreadSafe; import java.util.Set; /** * @author zhen * @Date 2018/11/23 11:02 * 对锁进行分解 */ @ThreadSafe public class ServerStatus { @GuardedBy("this") public final Set<String> users; @GuardedBy("this") public final Set<String> queries; public ServerStatus(Set<String> users, Set<String> queries) { this.users = users; this.queries = queries; } /* public synchronized void addUser(String u) { users.add(u); } public synchronized void addQuery(String q) { queries.add(q); } public synchronized void removeUser(String u) { users.remove(u); } public synchronized void removeQuery(String q) { queries.remove(q); }*/ //使用锁分解技术 public void addUser(String u) { synchronized(users){ users.add(u); } } public void addQuery(String q) { synchronized(queries){ queries.add(q); } } public void removeUser(String u) { synchronized(users){ users.remove(u); } } public void removeQuery(String q) { synchronized(queries){ queries.remove(q); } } }
package chapter10; import common.ThreadSafe; /** * @author zhen * @Date 2018/11/23 11:12 * 在基于散列的Map中使用锁分段技术 */ @ThreadSafe public class StripedMap { //同步策略:buckets[n]由locks[n%N_LOCKS]来保护 private static final int N_LOCKS = 16; private final Node[] buckets; private final Object[] locks; private static class Node { Node next; String key; Object value; } 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() { for (int i = 0; i < buckets.length; i++) { synchronized (locks[i % N_LOCKS]){ buckets[i] = null; } } } }

浙公网安备 33010602011771号