Java锁与性能相关知识复习与整理
来源:互联网 发布:图库软件下载 编辑:程序博客网 时间:2024/06/06 00:21
Java锁相关技巧:
锁无罪,竞争其罪
如果你在多线程代码中碰到了性能问题,你肯定会先抱怨锁。毕竟,从“常识”来讲,锁的性能是很差的,并且还限制了程序的可伸缩性。如果你怀揣着这样的想法去优化代码并删除锁的话,最后你肯定会引入一些难缠的并发BUG。
因此分清楚竞争锁与无竞争锁的区别是很有必要的。如果一个线程尝试进入另一个线程正在执行的同步块或者方法时,便会出现锁竞争。第二个线程就必须等待前一个线程执行完这个同步块并释放掉监视器(monitor)。如果只有一个线程在执行这段同步的代码,这个锁就是无竞争的。
事实上,JVM中的同步已经针对这种无竞争的情况进行了优化,对于绝大多数应用而言,无竞争的锁几乎是没有任何额外的开销的。因此,出了性能问题不能光怪锁,你得怪竞争锁。在明确了这点以后 ,我们来看下如何能减少锁的竞争或者竞争的时间。
保护数据而非代码
实现线程安全最快的方法就是直接将整个方法上锁。比如说下面的这个例子,这是在线扑克游戏服务端的一个简单的实现:
class
GameServer {
public
Map<<String, List<Player>> tables =
new
HashMap<String, List<Player>>();
public
synchronized
void
join(Player player, Table table) {
if
(player.getAccountBalance() > table.getLimit()) {
List<Player> tablePlayers = tables.get(table.getId());
if
(tablePlayers.size() <
9
) {
tablePlayers.add(player);
}
}
}
public
synchronized
void
leave(Player player, Table table) {
/*body skipped for brevity*/}
public synchronized void createTable() {/*body skipped for brevity*/}
public synchronized void destroyTable(Table table) {/*body skipped for brevity*/
}
}
作者的想法是好的——就是当新的玩家加入的时候,必须得保证桌上的玩家的数量不能超过9个。
不过这个上锁的方案更适合加到牌桌上,而不是玩家进入的时候——即便是在一个流量一般的扑克网站上,这样的系统也肯定会由于线程等待锁释放而频繁地触发竞争事件。被锁住的代码块包含了帐户余额以及牌桌上限的检查,这里面很可能会包括一些很昂贵的操作,这样不仅会容易触发竞争并且使得竞争的时间变长。
解决问题的第一步就是要确保你保护的是数据,而不是代码,先将同步从方法声明移到方法体里。在上面这个简短的例子中,刚开始好像能修改的地方并不多。不过我们考虑的是整个GameServer类,而不只限于这个join()方法:
class
GameServer {
public
Map<String, List<Player>> tables =
new
HashMap<String, List<Player>>();
public
void
join(Player player, Table table) {
synchronized
(tables) {
if
(player.getAccountBalance() > table.getLimit()) {
List<Player> tablePlayers = tables.get(table.getId());
if
(tablePlayers.size() <
9
) {
tablePlayers.add(player);
}
}
}
}
public
void
leave(Player player, Table table) {
/* body skipped for brevity */}
public void createTable() {/* body skipped for brevity */}
public void destroyTable(Table table) {/* body skipped for brevity */
}
}
这看似一个很小的改动,却会影响到整个类的行为。当玩家加入牌桌 时,前面那个同步的方法会锁在GameServer的this实例上,并与同时想离开牌桌(leave)的玩家产生竞争行为。而将锁从方法签名移到方法内部以后,则将上锁的时机往后推迟了,一定程度上减小了竞争的可能性。
缩小锁的作用域
现在我们已经确保保护的是数据而不是代码了,我们得再确认锁住的部分都是必要的——比如说,代码可以重写成这样 :
public
class
GameServer {
public
Map<String, List<Player>> tables =
new
HashMap<String, List<Player>>();
public
void
join(Player player, Table table) {
if
(player.getAccountBalance() > table.getLimit()) {
synchronized
(tables) {
List<Player> tablePlayers = tables.get(table.getId());
if
(tablePlayers.size() <
9
) {
tablePlayers.add(player);
}
}
}
}
//other methods skipped for brevity
}
现在检查玩家余额的这个耗时操作就在锁作用域外边了。注意到了吧,锁的引入其实只是为了保护玩家数量不超过桌子的容量而已,检查帐户余额这个事情并不在要保护的范围之内。
分拆锁
再看下上面这段代码,你会注意到整个数据结构都被同一个锁保护起来了。考虑到这个数据结构中可能会存有上千张牌桌,出现竞争的概率还是非常高的,因此保护每张牌桌不超出容量的工作最好能分别来进行。
对于这个例子而言,为每张桌子分配一个独立的锁并非难事,代码如下:
public
class
GameServer {
public
Map<
String
, List<Player>> tables =
new
HashMap<
String
, List<Player>>();
public
void
join(Player player, Table table) {
if
(player.getAccountBalance() > table.getLimit()) {
List<Player> tablePlayers = tables.
get
(table.getId());
synchronized (tablePlayers) {
if
(tablePlayers.size() <
9
) {
tablePlayers.add(player);
}
}
}
}
//other methods skipped for brevity
}
现在我们把对所有桌子同步的操作变成了只对同一张桌子进行同步,因此出现锁竞争的概率就大大减小了。如果说桌子中有100张桌子的话,那么现在出现竞争的概率就小了100倍。
使用并发的数据结构
另一个可以改进的地方就是弃用传统的单线程的数据结构,改为使用专门为并发所设计的数据结构。比如说,可以用ConcurrentHashMap来存储所有的扑克桌,这样代码就会变成这样:
public
class
GameServer {
public
Map<String, List<Player>> tables =
new
ConcurrentHashMap<String, List<Player>>();
public
synchronized
void
join(Player player, Table table) {
/*Method body skipped for brevity*/}
public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/
}
public
synchronized
void
createTable() {
Table table =
new
Table();
tables.put(table.getId(), table);
}
public
synchronized
void
destroyTable(Table table) {
tables.remove(table.getId());
}
}
join()和leave()方法的同步操作变得更简单了,因为我们现在不用再对tables进行加锁了,这都多亏了ConcurrentHashMap。然而,我们还是要保证每个tablePlayers的一致性。因此这个地方ConcurrentHashMap帮不上什么忙。同时我们还得在createTable()与destroyTable()方法中创建新的桌子以及销毁桌子,这对ConcurrentHashMap而言本身就是并发的,因此你可以并行地增加或者减少桌子的数量。
其它的技巧及方法
- 降低锁的可见性。在上述例子中,锁是声明为public的,因此可以被别人所访问到,你所精心设计的监视器可能会被别人锁住,从而功亏一篑。
- 看一下java.util.concurrent.locks包下面有哪些锁策略对你是有帮助的。(参考ConcurrentHashMap()类点击打开链接)
- 使用原子操作。上面这个例子中的简单的计数器其实并不需要进行加锁。将计数的Integer换成AtomicInteger对这个场景来说就绰绰有余了。
希望本文对你解决锁竞争的问题能有所帮助,不管你有没有使用我们的Plumbr所提供的自动化锁检测方案,还是自己手动从线程dump信息中提取信息。
=======================================================================================
Java中不同压缩算法的性能比较
- JDK GZIP ——这是一个压缩比高的慢速算法,压缩后的数据适合长期使用。JDK中的java.util.zip.GZIPInputStream / GZIPOutputStream便是这个算法的实现。
- JDK deflate ——这是JDK中的又一个算法(zip文件用的就是这一算法)。它与gzip的不同之处在于,你可以指定算法的压缩级别,这样你可以在压缩时间和输出文件大小上进行平衡。可选的级别有0(不压缩),以及1(快速压缩)到9(慢速压缩)。它的实现是java.util.zip.DeflaterOutputStream / InflaterInputStream。
- LZ4压缩算法的Java实现——这是本文介绍的算法中压缩速度最快的一个,与最快速的deflate相比,它的压缩的结果要略微差一点。如果想搞清楚它的工作原理,我建议你读一下这篇文章。它是基于友好的Apache 2.0许可证发布的。
- Snappy——这是Google开发的一个非常流行的压缩算法,它旨在提供速度与压缩比都相对较优的压缩算法。我用来测试的是这个实现。它也是遵循Apache 2.0许可证发布的
- 如果你认为数据压缩非常慢的话,可以考虑下LZ4(快速)实现,它进行文本压缩能达到大约320Mb/秒的速度——这样的压缩速度对大多数应用而言应该都感知不到。
- 如果你受限于无法使用第三方库或者只希望有一个稍微好一点的压缩方案的话,可以考虑下使用JDK deflate(lvl=1)进行编解码——同样的文件它的压缩速度能达到75Mb/秒。
- ====================================================================================
- Java字符串性能优化
========================================================================================
- 当转化成字符串的时候,应当避免使用”"串进行转化。使用合适的String.valueOf方法或者包装类的toString(value)方法。
- 尽量使用StringBuilder进行字符串拼接。检查下老旧码,把那些能替换掉的StringBuffer也替换成它。
- 使用Java 6 update 20引入的-XX:+OptimizeStringConcat选项来提高字符串拼接的性能。在最近的Java7的版本中已经默认打开了,不过在Java 6_41还是关闭的。
String,StringBuffer,StringBuilder的区别
String对象是不可变的,在说原因的时候没说清,其实看看String源码就知道了
在new String的时候,String 中的3个成员变量value,count,offset都是final的,当然String类也是final的,所以一旦初始化后不能修改的。
StringBuffer,与StringBuilder都实现了相同的接口,而且都继承相同的父类,不同的是,StringBuffer的大部分方法都是同步的,所以是线程安全,StringBuilder没有同步,所以通常情况下效率上StringBuilder是优于StringBuffer的。
StringBuffer与StringBuilder随着append会扩大value[]的容量,这里具体做法是使用System类中的arraycopy方法拷贝,这个方法是调用底层本地方法来处理的,类似于直接使用C的指针操作,比较快。
Java死锁的故障排查和解决
==================================================================================================================================本文将重温这个经典的线程问题,并总结提出了关键的故障排除和解决方法。我也将基于我自己的多线程的故障排除经验的主题来扩展。
Java deadlock: what is it?
一个真正的Java死锁基本上可以被描述为一种情况,两个或多个线程被永久阻塞,彼此等待。这种情况和其他更多常见的“day-to-day”线程问题的模式如锁争用和线程比赛,等待阻塞的IO调用等是不同的。这样的锁顺序死锁情况可以如下表示:
线程A & 线程B尝试以不同的顺序获得2个锁是致命的。一旦线程达到死锁的状态,它们永远无法恢复,迫使你重启受影响的JVM进程。
Heinz也描述了另外类型的死锁:资源死锁。这是我迄今经历过的Java EE企业级系统故障排查中最常见的线程问题模式。一个资源死锁本质上是一个场景,一个或多个线程正在等待获取的资源绝不会被提供,如JDBC池枯竭。
Lock-ordering deadlocks
你或许知道我对JVM thread dump analysis相当感兴趣,这是参与Java/Java EE开发或生产支持相当重要的技能。好消息是Java级别的死锁,可以很容易地被大多数JVM线程转储格式识别出(HotSpot,IBM VM...),因为它们包含一个本地死锁检测机制,这实际上通过执行堆栈跟踪将为你展示参与线程真正的Java级别的死锁情况。JVM线程dump可以由你选择的工具所获得,如VisualVM,jstack或基于Unix OS的kill -3 <PID>。当运行lab 1之后找出下面JVM Java级别的死锁检测部分:
现在,这是比较容易的部分......分析工作的核心是要理解为什么这样的参与线程死锁的情况在首位。锁顺序死锁可以从应用程序代码被触发,但除非你是从事高并发编程,罪魁祸首很可能是代码来自第三方API或框架(您正在使用或实际的Java EE容器本身)。
现在,让我们来回顾下Heinz提出的锁顺序死锁解决策略:
# Deadlock resolution by global ordering (see lab1 solution)
- Essentially involves the definition of a global ordering for the locks that would always prevent deadlock (please see lab1 solution)
# Deadlock resolution by TryLock (see lab2 solution)
- Lock the first lock
- Then try to lock the second lock
- If you can lock it, you are good to go
- If you cannot, wait and try again
上面的策略可以通过Java Lock & ReantrantLock来实现,这可以令你灵活地设置一个等待超时,以防止在锁定时间过长的情况下导致线程饥饿。
01
public
interface
Lock {
02
03
void
lock();
04
05
void
lockInterruptibly()
throws
InterruptedException;
06
07
boolean
tryLock();
08
09
boolean
tryLock(
long
timeout, TimeUnit unit)
10
11
throws
InterruptedException;
12
13
void
unlock();
14
15
Condition newCondition();
16
17
}
如果你看过JBoss AS7 的实现,你会发现它的核心实现层使用了Lock & ReantrantLock,例如:
- 部署服务
- EJB3 实现
- 集群和会话管理
- 内部缓存 & 数据结构 (LRU, ConcurrentReferenceHashMap…)
现在按照Heinz的观点,死锁的解决策略#2可以是相当有效的,但也需要合适的对待,如通过finally{}块释放所有持锁,否则你会将死锁情况变成活锁。
Resource deadlocks
现在,让我们继续探讨资源死锁情况。我很高兴,Heinz的实验室#3覆盖这个,因为从我的经验,这是目前最常见的“死锁”的情景,你会看到,特别是如果你正在开发和支持大型分布式Java EE的生产系统。
概括如下:
- 资源锁不是真正的Java级别的死锁
- JVM的线程转储不会神奇地要你这些类型的死锁。这意味着更多的工作,为您分析和理解这个问题为出发点。
- 当你刚开始学习如何读取线程dump,线程dump分析可能是特别混乱的,因为线程常常会显示为运行状态与阻塞状态的Java级别的死锁。现在,重要的是要记住,线程状态不是这种问题的类型,比如这一重要运行状态!=健康的状态。
- 该分析方法与Java级别的死锁有很大的不同。你必须采取多线程dump快照,并确定每个快照之间的线程问题/等待模式。您将可以看到线程不动,如等待线程从池中或已获得这样资源的线程中获取资源...
- 线程dump分析是不是唯一重要的数据点/因素。你需要收集其他线程正在等待的资源、整个中间件或环境健康的统计数据。这些因素的组合可以令你得出解决策略根本原因的结论,可能或可能不涉及代码的改动。
我会尽快给你更多的线程dump问题的模式,但首先,请确保您熟悉的JVM线程dump为起点的基本原则。
总结
- Java锁与性能相关知识复习与整理
- TCP/IP相关知识复习与总结(https/网络程序性能分析)
- JAVA相关知识复习
- jsp与tomcat相关知识整理
- jsp与tomcat相关知识整理
- Bitmap 优化与相关知识整理
- 阿里巴巴笔试题--二叉树(知识复习与整理)
- 电子商务复习笔记二:电子商务相关知识与技术
- 爬虫整理与复习
- 重新整理struts2与spring整合相关知识
- InnoDB表与索引结构相关知识整理
- OpenCV 实现人脸检测与相关知识整理
- perl时间相关模块解析与知识整理
- 卫星定位与导航相关知识的整理
- IPTV与VoIP相关知识整理(临时存储)
- java继承与接口相关知识
- zerocopy与java知识相关小记
- java线程相关知识整理
- 【设计模式】简单工厂VS工厂方法
- Oracle修改密码,及修改密码后登录不了的问题
- Combination Sum --- LeetCode
- android编译笔记
- ARP报文格式
- Java锁与性能相关知识复习与整理
- 社説 20150129 教科書の慰安婦 誤解を招く表現は訂正したい
- attrs.xml文件中属性类型format值的格式
- 19. Remove Nth Node From End of List Leetcode Python
- android ble 蓝牙4.0开发日志
- 网络基本功(二十三):Wireshark抓包实例诊断TCP连接问题
- uva 539 The Settlers of Catan(回溯)
- 为ArrayList去重
- 当浏览器页面放缩时......