java并发基础(六)--- 活跃性、性能与可伸缩性
《java并发编程实战》的第9章主要介绍GUI编程,在实际开发中实在很少见到,所以这一章的笔记暂时先放一放,从第10章开始到第12章是第三部分,也就是活跃性、性能、与测试,这部分的知识偏理论多一些,但是尽量能用代码讲明白的问题就不用文字,话不多说,进入正题。
一、死锁
在学习java基础的时候就听老师讲过“哲学家就餐”的例子,时间久了具体是怎么回事也容易忘,这里重新整理下。5个哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(不是5双),并且每两个人中间放一根筷子,每个人需要一双筷子才能吃到东西,如果每个人都立即抓住自己左边的筷子,并等待自己右边的筷子空出来,同时又不肯放弃自己已经拿到的筷子,那么每个人都会饿死。在java中就是,线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么会产生死锁,总结一下,也就是多个线程由于存在环路的锁依赖关系而永远等待下去,所以会产生死锁,而java程序是无法从死锁中恢复过来的,所以开发时要尤其注意。
//容易发生死锁
public class LeftRightDeadLock{
private final Object left = new Object();
private final Object right = new Object(); public void leftRight(){
synchronized(left){
synchronized (right) {
System.out.println("left-right");
}
}
} public void rightLeft(){
synchronized (right) {
synchronized (left) {
System.out.println("right-left");
}
}
}
}
上面这种形式是最简单的死锁,叫锁顺序死锁,它发生死锁的原因是:两个线程试图以不同顺序访问相同的锁。反过来,也就是说,如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁。但是在实际开发中几乎不可能实现。
有些时候死锁并不那么容易被发现,如下面代码,它将资金从一个账户转入另一个账户,在开始转账之前,首先要获得这两个Account对象的锁,以确保通过原子方式来更新两个账户中的余额,看上去没什么毛病,但是如果有两个线程同时调用transferMoney,其中一个线程从X向Y转账,另一个线程从Y向X转账,那么就可能发生死锁。
//A线程:transferMoney(myAccount,yourAccount,10);
//B线程:transferMoney(yourAccount,myAccount,20);
public void transferMoney(Account fromAccount,
Account toAccount,
Amount amount) throws InsufficientFundsException{
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(account) < 0) {//账户余额不能为0
throw new InsufficientFundsException();
}else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
前面提到过,如果能让线程访问锁的顺序一致,可以避免锁顺序死锁,可以通过两个账户的hashCode判断,修改代码:
private static final Object tieLock = new Object(); public void transferMoney(Account fromAccount,
Account toAccount,
Amount amount) throws InsufficientFundsException{
class Helper{
public void transfer() throws InsufficientFundsException{
if (fromAccount.getBalance().compareTo(account) < 0) {//账户余额不能为0
throw new InsufficientFundsException();
}else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
} int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount); if (fromHash < toHash) {
synchronized (fromAccount) {
synchronized (toAccount) {
new Helper().transfer();
}
}
}else if (fromHash > toHash) {
synchronized (toAccount) {
synchronized (fromAccount) {
new Helper().transfer();
}
}
}else {
synchronized (tieLock) {
synchronized (fromAccount) {
synchronized (toAccount) {
new Helper().transfer();
}
}
}
}
}
这样就避免了上面提到的两个账户互相转账的问题,两个对象的hashCode如果相同,则又添加了一个锁,保证每次只有一个线程以未知顺序访问锁。你可能会问,为什么不直接在最外面再加一层锁呢,把hashCode的判断直接省去,肯定不能这样做,如果这样,那么转账就成了串行的了,这样毫无并发可言,好在System.identityHashCode发生哈希码相同的时候非常少。增加了代码,至少解决了问题,这样,这个方法至少能用了。
在大型网站中每天可能要执行数十亿次获取锁和释放锁的操作,只要有一次发生错误产生死锁,程序都会蹦掉,而且,有时候应用程序即使通过了压力测试也不可能找到所有潜在的死锁。很多潜在的死锁比上面的例子更加隐晦,这里就不写例子了,那么有什么最好的解决办法让我们尽量的避免锁顺序死锁呢,答案是在程序中始终使用开放调用。
什么是开放调用,我自己的理解是,在方法中调用某个外部方法时,没有持有任何锁,也就是说,锁用完了,赶紧释放掉。这也就是为什么不提倡直接用synchronized直接修饰方法的最重要的原因。我想获得哪个锁,我获得,用完了,赶紧释放掉,哪怕过几行代码我还要用到这个锁,我也不会多占用这个锁一秒钟。这个只能说是一个良好的编程习惯,但它可以尽量规避锁顺序死锁。
二、性能与可伸缩性
1、系统为什么要做成分布式
程序的性能由多个指标来衡量,比如服务时间、延迟时间、吞吐率、效率、可伸缩性以及容量等。服务时间和等待时间指某个任务需要多快才能完成,吞吐量指在计算资源一定的情况下,能完成多少工作。可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力相应地增加。
性能的多快和多少是完全独立的,有时是互相矛盾的,我们熟悉的mvc三层结构是彼此独立的,并且可能由不同系统处理,这个例子很好的说明了提高可伸缩性通常会造成性能损失的原因,如果三层在单个系统中,处理第一个请求的时候其性能肯定高于将程序分成多层并将不同层次分布到多个系统时的性能。(LZ在面试时曾被问到具体说说是哪里带来了延迟,LZ当时只回答是网络通信,系统调用、数据复制之间的延迟,结果面试官很不满意,一再追问,大家可以自行百度,当做一个面试题记下来,预防面试官为难。)然而,当这种单一系统到达自身处理能力的极限时,会遇到一个很严重的问题:要进一步处理大量请求会非常难,因此,通常会接受每个请求执行更长时间或者消耗更多资源来换取更高的负载,这也就是为什么系统分布式的原因。
2、线程带来的问题
(1)上下文切换
如果可运行的线程数大于CPU数量,那么OS会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU,这将导致一次上下文切换,也就带来一些开销。
(2)锁竞争
当线程由于等待某个锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去,这也可能导致上下文切换。
3、减少锁竞争
通过上面介绍,串行操作降低可伸缩性,上下文切换降低性能,锁竞争会同时导致这两个问题,因此减少锁竞争能提高性能和可伸缩性。在并发程序中,对可伸缩性最主要威胁就是独占方式的资源锁。那怎么做呢,上面提到过了,开放调用,用完锁,赶紧释放,还有就是减少锁的粒度:锁分解和锁分段。这块内容LZ打算单独写一份笔记。
这块内容理论的东西很多,很多东西很难展开介绍,先看下这几个点吧,其实所有的知识点都指向如何避免锁竞争这个事情上,开放调用是一个办法,但是作用没有锁分段大。LZ也是刚开始用博客记录一些新知识,可能很多时候写东西写不到重点,我自己也尽量避免这个问题,慢慢来吧,很羡慕面对新知识能一下子抓住重点的大神,LZ只是一个想往上再走一步的菜鸟,各位多提意见,感谢大家。
java并发基础(六)--- 活跃性、性能与可伸缩性的更多相关文章
- java并发编程(4)性能与可伸缩性
性能与可伸缩性 一.Amdahl定律 1.问题和资源的关系 在某些问题中,资源越多解决速度越快:而有些问题则相反: 注意:每个程序中必然有串行的部分,而合理的分析出串行和并行的部分对程序的影响极大:串 ...
- java并发基础(二)
<java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...
- Java 并发基础
Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...
- java并发基础及原理
java并发基础知识导图 一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...
- 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!
本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...
- java并发基础(五)--- 线程池的使用
第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...
- Java并发基础概念
Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...
- 【Java并发基础】安全性、活跃性与性能问题
前言 Java的多线程是一把双刃剑,使用好它可以使我们的程序更高效,但是出现并发问题时,我们的程序将会变得非常糟糕.并发编程中需要注意三方面的问题,分别是安全性.活跃性和性能问题. 安全性问题 我们经 ...
- Java并发基础:进程和线程之由来
转载自:http://www.cnblogs.com/dolphin0520/p/3910667.html 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程. ...
随机推荐
- JavaScript内部原理实践——真的懂JavaScript吗?(转)
通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3 JavaScript内部原理的文章, 从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解. 但 ...
- [转载]AngularJS之Factory vs Service vs Provider
http://www.oschina.net/translate/angularjs-factory-vs-service-vs-provider http://tylermcginnis.com/a ...
- 【CodeForces】983 E. NN country 树上倍增+二维数点
[题目]E. NN country [题意]给定n个点的树和m条链,q次询问一条链(a,b)最少被多少条给定的链覆盖.\(n,m,q \leq 2*10^5\). [算法]树上倍增+二维数点(树状数组 ...
- C++ 修饰符类型
C++ 修饰符类型 C++ 允许在 char.int 和 double 数据类型前放置修饰符.修饰符用于改变基本类型的含义,所以它更能满足各种情境的需求. 下面列出了数据类型修饰符: signed u ...
- TFS报表管理器无权限访问的配置
刚接触TFS,有太多的功能不能知道怎么配置,今天想了解一下TFS的报表功能,当登录TFS后,点击项目中的“查看报表”
- 使用maven命令终端构建一个web项目及发布该项目
构建环境: maven版本:3.3.9 系统平台:Windows7 x64 JDK版本:1.7 构建步骤: 1.打开maven安装目录,在地址栏输入cmd进入命令窗口 2.输入命令mvn archet ...
- Linux删除以减号开头的文件
2014年5月5日 10:33:47 原因:文件乱码了,乱码后以减号开头,删不掉 摘抄: 文件系统出现一个文件 -C.html 如何删除/新建?rm -- "-C.html" to ...
- jenkins的svn路径中文问题
今天弄Jenkins,我们的SVN代码路径是中文的,他娘的坑死我了,很没面子弄了俩点,网上方案试了好多,说装插件,修改Tomcat server.xml,基本没用,后来看到一个帖子写的方案蛮实用的,分 ...
- Ibatis.Net 执行存储过程学习(八)
首先在数据库创建存储过程: create proc [dbo].[usp_GetPersonById] @Id int as begin select Id,Name from Person wher ...
- 绘图: matplotlib核心剖析
参考:http://www.cnblogs.com/vamei/archive/2013/01/30/2879700.html http://blog.csdn.net/ywjun0919/artic ...