`

以全局的固定顺序获取多个锁来避免死锁

 
阅读更多

当两个或多个线程互相等待时被阻塞,就会发生死锁。例如,第一个线程被第二个线程阻塞,它在等待第二个线程持有的一个资源。而第二个线程在获得第一个线程持有的某个资源之前不会释放这个资源。由于第一个线程在获得第二个线程持有的那个资源之前不会释放它自己所持有的资源,而第二个线程在获得第一个线程持有的一个资源之前也不会释放它所持有的资源,于是这两个线程就被死锁。


在编写多线程代码时,死锁是最难处理的问题之一。因为死锁可能在最意想不到的地方发生,所以查找和修正它既费时又费力。例如,试考虑下面这段锁定了多个对象的代码。

public int sumArrays(int[] a1, int[] a2)

{

int value = 0;

int size = a1.length;

if (size == a2.length) {

synchronized(a1) { //1

synchronized(a2) { //2

for (int i=0; i<size; i++)

value += a1[i] + a2[i];

}

}

}

return value;

}

这段代码在求和操作中访问两个数组对象之前正确地锁定了这两个数组对象。它形式简短,编写也适合所要执行的任务;但不幸的是,它有一个潜在的问题。这个问题就是它埋下了死锁的种子,除非您在不同的线程中对相同的对象调用该方法时格外小心。要查看潜在的死锁,请考虑如下的事件序列:



  1. 创建两个数组对象,ArrayAArrayB


  2. 线程 1 用下面的调用来调用 sumArrays 方法:
    sumArrays(ArrayA, ArrayB);


  3. 线程 2 用下面的调用来调用 sumArrays 方法:
    sumArrays(ArrayB, ArrayA);


  4. 线程 1 开始执行 sumArrays 方法并在 //1 处获得对参数 a1 的锁,对于这个调用而言,它就是对 ArrayA 对象的锁。


  5. 然后在 //2 处,在线程 1 获得对 ArrayB 的锁之前被抢先。


  6. 线程 2 开始执行 sumArrays 方法并在 //1 处获得对参数 a1 的锁,对于这个调用而言,它就是对 ArrayB 对象的锁。


  7. 然后线程 2 在 //2 处试图获取对参数 a2 的锁,它是对 ArrayA 对象的锁。因为这个锁当前由线程 1 持有,所以线程 2 被阻塞。


  8. 线程 1 开始执行并在 //2 处试图获取对参数 a2 的锁,它是对 ArrayB 对象的锁。因为这个锁当前由线程 2 持有,所以线程 1 被阻塞。


  9. 现在两个线程都被死锁。

避免这种问题的一种方法是让代码按固定的全局顺序获取锁。在本例中,如果线程 1 和线程 2 按相同的顺序对参数调用 sumArrays 方法,就不会发生死锁。但是,这一技术要求,多线程代码的程序员在调用那些锁定作为参数传入的对象的方法时需要格外小心。在您遇到这种死锁并不得不进行调试之前,使用这一技术的应用程序似乎不切实际。


另外,您也可以将锁定顺序嵌入对象的内部。这允许代码查询它准备为其获得锁的对象,以确定正确的锁定顺序。只要即将锁定的所有对象都支持锁定顺序表示法,并且获取锁的代码遵循这一策略,就可避免这种潜在死锁的情况。


在对象中嵌入锁定顺序的缺点是,这种实现将使内存需求和运行时成本增加。另外,在上例中应用这一技术需要在数组中有一个包装对象,用来存放锁定顺序信息。例如,试考虑下面的代码,它由前面的示例修改而来,其中实现了锁定顺序技术:

class ArrayWithLockOrder

{
private static long num_locks = 0;
private long lock_order;
private int[] arr;

public ArrayWithLockOrder(int[] a)
{
arr = a;
synchronized(ArrayWithLockOrder.class) {
num_locks++; // 锁数加 1。
lock_order = num_locks; // 为此对象实例设置唯一的 lock_order。
}
}
public long lockOrder()
{
return lock_order;
}
public int[] array()
{
return arr;
}
}

class SomeClass implements Runnable
{
public int sumArrays(ArrayWithLockOrder a1,
ArrayWithLockOrder a2)
{
int value = 0;
ArrayWithLockOrder first = a1; // 保留数组引用的一个
ArrayWithLockOrder last = a2; // 本地副本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 确定并设置对象的锁定
{ // 顺序。
first = a2;
last = a1;
}
synchronized(first) { // 按正确的顺序锁定对象。
synchronized(last) {
int[] arr1 == a1.array();
int[] arr2 == a2.array();
for (int i=0; i<size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() {
//...
}
}


在第一个示例中,ArrayWithLockOrder 类是作为数组的一个包装提供的。每创建该类的一个新对象,该类就将 static num_locks 变量加 1。一个单独的 lock_order 实例变量被设置为 num_locks static 变量的当前值。这可以保证,对于该类的每个对象,lock_order 变量都有一个独特的值。lock_order 实例变量充当此对象相对于该类的其他对象的锁定顺序指示器。


请注意,static num_locks 变量是在 synchronized 语句中进行操作的。这是必须的,因为对象的每个实例共享该对象的 static 变量。因此,当两个线程同时创建 ArrayWithLockOrder 类的一个对象时,如果操作 static num_locks 变量的代码未作同步处理,该变量就可能被破坏。对此代码作同步处理可以保证,对于 ArrayWithLockOrder 类的每个对象,lock_order 变量都有一个独特的值。


此外还更新了 sumArrays 方法,以使它包括确定正确锁定顺序的代码。在请求锁之前,将查询每个对象以获得它的锁定顺序。编号较小的首先被锁定。此代码可以保证,不管各对象是以什么顺序传给此方法,它们总是被以相同的顺序锁定。


static num_locks 域和 lock_order 域都是作为 long 类型实现的。long 数据类型是作为 64 位有符号二进制补码整数实现的。这意味着在创建 9,223,372,036,854,775,807 个对象之后,num_lockslock_order 的值将重新开始。您未必会达到这个极限,但在适当的条件下这是可能发生的。


实现嵌入的锁定顺序需要投入更多的工作,使用更多的内存,并会延长执行时间。但是,如果您的代码中可能存在这些类型的死锁,您也许会发现值得这样做。如果您无法承受额外的内存和执行开销,或者不能接受 num_lockslock_order 域重新开始的可能性,则您在建立锁定对象的预定义顺序时应该仔细斟酌。

分享到:
评论

相关推荐

    concurrent 多线程 教材

    11 以全局的固定顺序获取多个锁来避免死锁 12 Java单例对象同步问题探讨 13 Java 理论与实践: 描绘线程安全性 (2) 14 编写高效的线程安全类 (2) 15 轻松使用线程 同步不是敌人.mht 16 轻松使用线程 减少争用....

    python cookbook(第3版)

    1.1 解压序列赋值给多个变量 1.2 解压可迭代对象赋值给多个变量 1.3 保留最后N个元素 1.4 查找最大或最小的N个元素 1.5 实现一个优先级队列 1.6 字典中的键映射多个值 1.7 字典排序 1.8 字典的运算 1.9 ...

    浅谈Python线程的同步互斥与死锁

    共享资源:多个进程或者线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序。  3. ...

    数据库系统实现

    7.6.4 通过动态编程来选择连接顺序和分组 7.6.5 带有更具体的代价函数的动态编程 7.6.6 选择连接顺序的贪婪算法 习题 7.7 物理查询计划选择的完成 7.7.1 选取选择方法 7.7.2 选取连接方法 7.7.3 流水...

    ACE中的DoubleCheckedLocking模式

    北京火龙果软件工程技术中心意图无论什么时候当临界区中的代码仅仅需要加锁一次,同时当其获取锁的时候必须是线程安全的,可以用DoubleCheckedLocking模式来减少竞争和加锁载荷。动机1、标准的单例。开发正确的有效...

    C#编程经验技巧宝典

    24 &lt;br&gt;0050 using关键字的用法 24 &lt;br&gt;0051 变量的作用域 25 &lt;br&gt;2.5 其他 26 &lt;br&gt;0052 有效使用this对象 26 &lt;br&gt;0053 如何声明变量 26 &lt;br&gt;0054 如何声明相同类型的多个变量 26 ...

    超级有影响力霸气的Java面试题大全文档

    与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。...

    java 面试题 总结

    与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。...

    oracle学习文档 笔记 全面 深刻 详细 通俗易懂 doc word格式 清晰 连接字符串

    认真听课、多思考问题、多动手操作、有问题一定要问、多参与讨论、多帮组同学 五、 体系结构 oracle的体系很庞大,要学习它,首先要了解oracle的框架。oracle的框架主要由物理结构、逻辑结构、内存分配、后台进程...

    ORACLE9i_优化设计与系统调整

    §7.2 关于创建多个Oracle实例问题 93 §7.3 Oracle系统安装后的优化基础工作 94 §7.3.1 Oracle系统有关目录所有文件的保护 94 §7.3.2 避免新用户使用默认system系统表空间 94 §7.4 Oracle系统所在服务器的独立性...

    delphi 开发经验技巧宝典源码

    0109 使用Pred函数获取顺序类型表达式的前驱 72 0110 使用Succ函数获取顺序类型表达式的后继 73 4.4 进制转换相关函数 73 0111 如何将二进制转换为八进制 73 0112 如何将二进制转换为十进制 75 0113 如何...

    入门学习Linux常用必会60个命令实例详解doc/txt

    不同Linux发行版的命令数量不一样,但Linux发行版本最少的命令也有200多个。这里笔者把比较重要和使用频率最多的命令,按照它们在系统中的作用分成下面六个部分一一介绍。 ◆ 安装和登录命令:login、shutdown、...

    oracle数据库经典题目

    当多个用户同时连接同一个实例时,SGA区数据供多个用户共享,所以SGA区又称为共享全局区。用户对数据库的各种操作主要在SGA区中进行。该内存区随数据库实例的创建而分配,随实例的终止而释放。PGA区是在用户进程连接...

    delphi 开发经验技巧宝典源码06

    0109 使用Pred函数获取顺序类型表达式的前驱 72 0110 使用Succ函数获取顺序类型表达式的后继 73 4.4 进制转换相关函数 73 0111 如何将二进制转换为八进制 73 0112 如何将二进制转换为十进制 75 0113 如何...

    mysql数据库my.cnf配置文件

    # 为提高性能,MySQL可以以循环方式将日志文件写到多个文件。推荐设置为3 innodb_max_dirty_pages_pct = 90 # innodb主线程刷新缓存池中的数据,使脏数据比例小于90% innodb_lock_wait_timeout = 120 # InnoDB事务在...

    SQL Server 2008管理员必备指南(超高清PDF)Part1

    9.9.3 多个登录的对象权限 第Ⅲ部分 SQL Server 2008的数据管理 第10章 操作架构、表、索引和视图 10.1 处理架构 10.1.1 创建架构 10.1.2 修改架构 10.1.3 将对象移动到新架构 10.1.4 删除架构 10.2 表的概况 10.3 ...

    SQL Server 2008管理员必备指南(超高清PDF)Part3

    9.9.3 多个登录的对象权限 第Ⅲ部分 SQL Server 2008的数据管理 第10章 操作架构、表、索引和视图 10.1 处理架构 10.1.1 创建架构 10.1.2 修改架构 10.1.3 将对象移动到新架构 10.1.4 删除架构 10.2 表的概况 10.3 ...

    SQL Server 2008管理员必备指南(超高清PDF)Part2

    9.9.3 多个登录的对象权限 第Ⅲ部分 SQL Server 2008的数据管理 第10章 操作架构、表、索引和视图 10.1 处理架构 10.1.1 创建架构 10.1.2 修改架构 10.1.3 将对象移动到新架构 10.1.4 删除架构 10.2 表的概况 10.3 ...

    SQL.Server.2008管理员必备指南.part2.rar(2/4)

     9.9.3 多个登录的对象权限 270  第Ⅲ部分 SQL Server 2008的数据管理  第10章 操作架构、表、索引和视图 275  10.1 处理架构 275  10.1.1 创建架构 276  10.1.2 修改架构 278  10.1.3 将对象移动到新架构 ...

Global site tag (gtag.js) - Google Analytics