Java Concurrency in Practice 读书笔记 第十章

时间:2021-01-17 23:41:57

粗略看完《Java Concurrency in Practice》这部书,确实是多线程/并发编程的一本好书。里面对各种并发的技术解释得比较透彻,虽然是面向Java的,但很多概念在其他语言的并发编程中,也可以用到。因此,开始写下其读书笔记,归纳总结。

闲话少说,从第十章开始,先上思维导图:

Java Concurrency in Practice 读书笔记 第十章

本章的重点,是死锁以及死锁的分析和避免方法

10.1.1 锁的顺序产生死锁:

public class WorkerThread implements Runnable{

    private LeftRightDeadlock lock;
private boolean isLeft; public WorkerThread(LeftRightDeadlock lock, boolean isLeft) {
  super();
  this.lock = lock;
  this.isLeft = isLeft;
} @Override
public void run() {
  if(isLeft){
  lock.leftRight();
  }else{
  lock.rightLeft();
   }
} }
public class LeftRightDeadlock {

    private final Object leftLock = new Object();
private final Object rightLock = new Object(); public void leftRight() {
System.out.println("left acquiring lock on leftLock");
synchronized (leftLock) {
System.out
.println("left acquired lock on leftLock, acquiring lock on rightLock");
sleep(); // key.
synchronized (rightLock) {
System.out.println("left is Working...");
}
}
} public void rightLeft() {
System.out.println("right acquiring lock on rightLock");
synchronized (rightLock) {
System.out
.println("right acquired lock on rightLock, acquiring lock on leftLock");
sleep(); // key.
synchronized (leftLock) {
System.out.println("right is Working..."); }
}
} private void sleep() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} /**
* @param args
*/
public static void main(String[] args) {
LeftRightDeadlock lock = new LeftRightDeadlock();
Thread thread1 = new Thread(new WorkerThread(lock, true));
Thread thread2 = new Thread(new WorkerThread(lock, false));
thread1.start();
thread2.start();
} }

上述代码中,输出有可能是:

left acquiring lock on leftLock
right acquiring lock on rightLock
left acquired lock on leftLock, acquiring lock on rightLock
right acquired lock on rightLock, acquiring lock on leftLock

然后就死锁了。死锁的原因就在于两个线程试图经过不同的顺序取得相同的锁(The deadlock in LeftRightDeadlock came about because the two threads attempted to acquire the same locks in a different order):

thread1 -> lock left -> try to lock right -> wait forever

thread2       -> lock right -> try to lock left -> wait forever

10.1.2 动态锁的顺序产生死锁:

public void transferMoney(Account fromAccount, Account toAccount,
BigDecimal amount) throws Exception { synchronized (fromAccount) {
synchronized (toAccount) {
if(fromAccount.getBalance().compareTo(amount)<0){
throw new Exception("Not enough money!!!");
}else{
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
} }

假设有两个线程,分别调用:

transferMoney(myAccount, yourAccount, 10);
transferMoney(yourAccount, myAccount, 20);

同样,transferMoney会发生死锁。原因与10.1.1类似,都是内嵌锁导致死锁,只不过这里隐蔽一点,因为锁fromAccount和锁toAccount是动态的。为了解决这个问题,我们必须制定锁的顺序,并且在程序中,获得锁的顺序始终遵守这个约定。

一个制定锁顺序的方法是,利用锁的hashCode确定顺序。如果两个锁的hashCode刚好相等,便引入第三个tie-breaking锁,以保证只有一个线程以未知顺序获得锁:

 private static final Object tieLock = new Object();

     public void transferMoney2(Account fromAccount, Account toAccount,
BigDecimal amount) throws Exception { int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount); if(fromHash<toHash){
synchronized (fromAccount) {
  synchronized (toAccount) {
  if(fromAccount.getBalance().compareTo(amount)<0){
    throw new Exception("Not enough money!!!");
  }else{
    fromAccount.debit(amount);
    toAccount.credit(amount);
  }
  }
}
}else if(toHash<fromHash){
synchronized (toAccount) {
  synchronized (fromAccount) {
  if(fromAccount.getBalance().compareTo(amount)<0){
    throw new Exception("Not enough money!!!");
  }else{
    fromAccount.debit(amount);
    toAccount.credit(amount);
  }
  }
}
}else{
synchronized (tieLock) {
  synchronized (fromAccount) {
  synchronized (toAccount) {
    if(fromAccount.getBalance().compareTo(amount)<0){
    throw new Exception("Not enough money!!!");
  }else{
    fromAccount.debit(amount);
    toAccount.credit(amount);
  }
  }
  }
}
} }

上述代码看起来有些臃肿,但基本的思路就是,利用hashCode,制定了根据hashCode,由小到大获得锁的规则,来解决死锁的问题。当然,如果hashCode经常冲突的话,就必须使用tieLock,那么这里就会成为并发的瓶颈。

假设Account具有一个唯一的,不可变的索引,那么制定锁的顺序就不需要hashCode且省去tie-breaking锁。

10.1.3 协作对象间的死锁

对比起10.1.1和10.1.2的死锁情况, 协作对象间的死锁更不明显,更不容易被发现:

 public class Taxi {

     private Point location, destination;
private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher){
  this.dispatcher = dispatcher;
} public synchronized Point getLocation(){
  return location;
} public synchronized void setLocation(Point location){
  this.location = location;
  if(location.equals(destination)){
  dispatcher.notifyAvailable(this);
  }
} } public class Dispatcher { private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis; public Dispatcher(){
  taxis = new HashSet<Taxi>();
  availableTaxis = new HashSet<Taxi>();
} public synchronized void notifyAvailable(Taxi taxi){
  availableTaxis.add(taxi);
} public synchronized Image getImage(){
  Image image = new Image();
  for (Taxi t: taxis){
  image.drawMarker(t.getLocation());
  }
return image;
} }

尽管没有方法显式地获得两个锁,但是也有可能出现死锁。原因如下:如果一个线程调用setLocation,检查已到达目的地,然后调用Dispatcher的notifyAvailable方法。由于setLocation和notifyAvailable都是synchronized方法,setLocation会获得Taxi的锁,而notifyAvailable会获得Dispatcher的锁;这时如果有线程调用getImage取得Dispatcher的锁,然后因为getImage里面的getLocation取得Taxi的锁,那么此时,两个锁被两个线程以不同的顺序占有,继而产生死锁风险。

所以,在持有锁的方法内,调用外部方法(alien method),则是产生死锁的一种警示。

10.1.4 开放调用:调用没有持有锁的方法。

使用开放调用,来解决协作间对象的死锁风险。通过减少synchronized块的使用,仅仅守护那些调用共享状态的操作,Taxi和Dispatcher可以被重构到使用开放调用,减少死锁风险。请看修改后的方法:

//Taxi
public synchronized Point getLocation(){
return location;
} public void setLocation(Point location){
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
} if(reachedDestination){
dispatcher.notifyAvailable(this);
}
} //Dispatcher
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
} public Image getImage(){
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t: copy){
image.drawMarker(t.getLocation());
}
return image;
}

要注意的是,这样的重构会将一个操作(如getImage)由原子操作变为非原子操作。必须仔细考虑这样的损失是否可以接受。

10.2 避免和诊断死锁。

大概来说有几个方法:

1.避免内嵌锁。内嵌锁是最常见的产生死锁的原因。上述的10.1.1、10.1.2、10.1.3本质上都是由于内嵌锁而导致死锁。

2.需要的时候才加锁(Lock Only What is Required)。可参考10.1.4

3.避免无限时地等待。此时可以利用定时锁tryLock

4.分析thread dump。至于如何分析,之后再补充。

参考:《Java Concurrency In Practice》

Java Concurrency in Practice 读书笔记 第十章的更多相关文章

  1. Java Concurrency in Practice&mdash&semi;&mdash&semi;读书笔记

    Thread Safety线程安全 线程安全编码的核心,就是管理对状态(state)的访问,尤其是对(共享shared.可变mutable)状态的访问. shared:指可以被多个线程访问的变量 mu ...

  2. Java Concurrency in Practice 读书笔记 第二章

    第二章的思维导图(代码迟点补上):

  3. java concurrency in practice读书笔记---ThreadLocal原理

    ThreadLocal这个类很强大,用处十分广泛,可以解决多线程之间共享变量问题,那么ThreadLocal的原理是什么样呢?源代码最能说明问题! public class ThreadLocal&l ...

  4. 《Java 8实战》读书笔记系列&mdash&semi;&mdash&semi;第三部分:高效Java 8编程(四):使用新的日期时间API

    https://www.lilu.org.cn/https://www.lilu.org.cn/ 第十二章:新的日期时间API 在Java 8之前,我们常用的日期时间API是java.util.Dat ...

  5. 《Java编程思想》读书笔记

    前言 这个月一直没更新,就是一直在读这本<Java编程思想>,这本书可以在Java业界被传神的一本书,无论谁谈起这本书都说好,不管这个人是否真的读过这本书,都说啊,这本书很好.然后再看这边 ...

  6. 《神经网络算法与实现-基于Java语言》的读书笔记

    文章提纲 全书总评 读书笔记 C1.初识神经网络 C2.神经网络是如何学习的 C3.有监督学习(运用感知机) C4.无监督学习(自组织映射) Rreferences(参考文献) 全书总评 书本印刷质量 ...

  7. 《深入理解 Java 内存模型》读书笔记

    ![img](https://mmbiz.qpic.cn/mmbiz_jpg/1flHOHZw6RtPu3BNx3zps1JhSmPICRw7QgeOmxOfTbCT3RLgIo4qRpn6xL4qg ...

  8. 转《深入理解 Java 内存模型》读书笔记

    转:https://mp.weixin.qq.com/s/2hA6u4hLEPWlTPdD-XB-bg 前提 <深入理解 Java 内存模型>程晓明著,该书在以前看过一遍,现在学的东西越多 ...

  9. Java编程思想读书笔记 第十章 内部类

    非静态内部类作用: 最基本的作用:名字隐藏和组织代码 用例:内部类不访问外部类的元素时可以直接new.(bad style!) 用例:通过外部类的非静态方法返回内部类的引用,这样隐含了内部类对象和其对 ...

随机推荐

  1. tp中使用分页技术

    1 public function showList() { $m_ld = D ( 'guangxi_ld' ); $page = I ( 'get.p', 1 ); // 在配置中获取分页值 $p ...

  2. c&plus;&plus; 二维数组传递

    c++ 二维数组传递 我们在传递二维数组时,对于新手来说,可能会存在某些问题,下面讲解几种传递方法 在讲解如何传递二维数组时,先看看如何动态new 二维数组 // 二维数组动态申请 int row , ...

  3. Forth scrum meeting - 2015&sol;10&sol;29

    今天下午,我们终于要到了MOOC服务器端开发人员的联系方式,于是我们小组派了三名同学去实验室与他们进行了交流,并咨询了他们一些关于API接口的问题.他们也对我们这个客户端的开发提出了一些建议. 开发团 ...

  4. divcss5布局

    一.ie9不支持line-height字体垂直居中兼容问题    原因:CSS中使用了中文字体,而中文字体使用汉字.如:font-family:"微软雅黑"   1.将中文字体汉字 ...

  5. C&num;基础&lpar;四&rpar;——ref与out的区别

    1.ref传进去的参数必须进行初始化,out不必int i;SomeMethod( ref i );//语法错误SomeMethod( out i );//通过 2.ref传进去的参数在函数内部可以直 ...

  6. Visual C&plus;&plus; 6&period;0编程环境的使用

    1.1 编制并运行程序的四部曲 (1)编辑(把程序代码输入,交给计算机). (2)编译(成目标程序文件.obj).编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟 ...

  7. css3动画实现旋转木马

    写旋转木马的时候,突发奇想想加个遮罩效果,那当然是用box-reflect属性了,然鹅,却被overflow:hidden坑了....... 写的效果就是不出来,太任性了有木有,代码无误呀,也没报错, ...

  8. js 图片压缩上传&lpar;纯js的质量压缩,非长宽压缩&rpar;

    下面是大神整理的demo,很实用,这里存一下备用,感谢大神! 此demo为大于1M对图片进行压缩上传 若小于1M则原图上传,可以根据自己实际需求更改. demo源码如下 <!DOCTYPE ht ...

  9. Caffe使用新版本CUDA和CuDNN

    因为一些原因还是需要使用别人基于Caffe的代码,但是代码比较老,默认不支持高版本的cuda或者cudnn 怎么办呢?基本上就是把最新官方Caffe-BVLC的几个关键文件拿过来替换即可. 脚本如下: ...

  10. eclipse总是自动跳到ThreadPoolExecutor解决办法

    出现这种状况是因为Eclipse默认开启挂起未捕获的异常(Suspend execution on uncaught exceptions),只要关闭此项就可以了. 解决方法:在eclipse中选择W ...