MIT的学习资料(reading部分)锁与同步学习笔记
系统构件题材及问题探讨
- 锁和同步
目标
-
了解一个锁是用于保护共有的可变数据
-
能够认识到的僵局以及知道战略,以防止它
-
知道监视器模式,并能够将其应用于一个数据类型
介绍
我们 定义的线的安全 数据类型或作为 行为时能正确使用从多个线程,无论如何那些线都是执行,而无需额外的协调 .
这里是总的原则:正确性的一个并行的程序不应当取决于意外事故的时间。
为实现这一正确性,我们列举的 四项战略对于使码的安全并发 :
- 限制 :不分享数据之间线,通过保持变量的数据点可以从仅仅一个线程。
- 不可变性 :使用共用的数据不可改变的,通过使用最后的变量和不变的类型。
- 使用现有的线程安全的数据类型 :使用一种数据类型,并协调为你。
- 同步 :预防线访问的数据共享在同一时间。 这就是我们用来实现一个线程安全的类型,但是我们没有讨论的时候。
在这读书,我们将讨论有关战略,4,使用同步实施自己的数据类型的安全共存并发。
同步
-
正确性的一个并行的程序不应当取决于意外事故的时间。
由于竞争条件造成的并发操纵的共享的可变数据是灾难性的错误—难以发现,很难再现,难以调试—我们需要一种方法并发的模块,该模块分享存储器 进行同步 。
锁 是一个同步的技术。 一个锁是一个抽象的概念,允许至多一个线程,以 自己 在一个时间。 保持锁 是怎么一线告诉其他主题:“我的工作有了这个东西,不要碰它现在。”
锁已经两个操作:
acquire
允许一个线程,以取得所有权的一个锁。 如果一个线试图获取锁定目前拥有由另外一个线程,它 块 ,直到其他线释放锁。 在这一点上,它将不得不与任何其他线,试图获取锁。 在大多数一线可以自己锁在一段时间。release
放弃所有权的锁,从而允许另外一个线程采取的所有权。
使用一把锁还告诉编译和处理,你使用共享存储器的同时,使登记册和缓存将被冲刷共用的存储。 这样可以避免问题 重新排序 ,确保所有人的一个锁总是在看最新数据。
阻挡 意味着,在一般情况下,这一线等待(而不做进一步的工作),直到事件的发生。
一个
acquire(l)
在线1块如果另外一个线程(说螺纹2)为保持锁定l
. 该事件等待是螺纹2执行release(l)
. 在这一点上,如果螺纹1可以获得l
它继续运行它的代码拥有所有权的锁。 这可能是另外一个线程(说螺纹3)也被阻止上acquire(l)
. 如果是这样,任一线1或3(获胜者是不确定的)将采取的锁l
并继续。 其他将继续块,在等待release(l)
再次。 -
银行账户的例子
- 我们的第一个例子共享内存的并发是一个 [银行提款机 ]在这里插入图片描述
图从这一例子是在右边。
银行中有几个现金机,所有这些都可以阅读和书写的同一账户的对象中存储器。
当然,没有任之间的协调,并行的读写的账户余额, [事情发生了可怕的错误 ](http://web.mit.edu/6.031/www/fa19/classes/19-concurrency/#interleaving).
- 为了解决这个问题使用锁,我们可以添加一个锁,保护每个银行帐户。 现在,在他们可以访问的或更新的一个帐户余额、现金机必须首先获得锁上的这帐户。 在图的权利,A和B两者都试图访问帐户1. B假设取得锁的第一个。 然后必须等待阅读或写的余额,直到B完成并释放锁。 这将确保一个和B是同步的,但是另一个取款机C是能够独立运行在不同的帐户(因为该帐户的保护不同锁定)。
僵局
-
当使用的前提下妥然和仔细地,锁可以防止竞争条件。 但后来另一个问题抬头。 因为使用锁需要线等(
acquire
块时,另外一个线程保持锁定),它就可能获得进入的情况下两个线程正在等待 对彼此 —并因此既不可能取得进展。 -
在图中的权利,假定A和B作同时进行转让的两个帐户之间在我们的银行。
-
一个账户之间转移的需要以锁定两个账户,所以这笔钱不能消失,从该系统。 A和B中的每一个获取锁定在他们各自的"从"帐户第一:获取上的锁帐户1和B取得的锁帐户2. 现在,每个人都必须获得锁定他们"对"账户:因此,一个正在等待B释放的帐户2锁,而B在等待释放的账户1锁。 僵局! A和B冻结在一个"致命的接受,"而账户都锁起来。
-
僵局 时发生并发的模块被困在等待彼此做到的事情。 僵局可能涉及超过两个模块:信号特征的僵局是一个 周期的依赖性 ,例如一个正在等待B这是等待C这是等待A. 没有人可以取得进展。
你还可以有僵局,不使用任何锁。 例如,消息传递系统可以体验到的僵局的时候消息缓冲区填补起来。 如果客户填补了服务器的缓冲区,与请求,然后 块 等待添加另一个请求,服务器可后,填写了客户的缓冲区的结果和随后的框本身。 因此客户等待的服务器和服务器等待的客户,也不可能取得进展,直到其他人做。 再次,僵局的产生。
发展中一个线程安全的抽象数据的类型
让我们看看如何使用同步执行一个线程安全安达泰.
你可以看到所有的代码这个例子在想: "编辑"缓冲例 . 你是 不是 预期的阅读和理解所有的代码。 所有相关部分摘录如下。
假设我们建设一个多用户编辑,如谷歌Docs,允许多个人连接和编辑,它在同一时间。 我们需要一个可变数据类型代表的文本在该文件中。 这里的接口;基本上,它表示一串插入和删除的操作:
/** An EditBuffer represents a threadsafe mutable * string of characters in a text editor. */ public interface EditBuffer { /** * Modifies this by inserting a string. * @param pos position to insert at (requires 0 <= pos <= current buffer length) * @param ins string to insert */ public void insert(int pos, String ins); /** * Modifies this by deleting a substring * @param pos starting position of substring to delete * (requires 0 <= pos <= current buffer length) * @param len length of substring to delete * (requires 0 <= len <= current buffer length - pos) */ public void delete(int pos, int len); /** * @return length of text sequence in this edit buffer */ public int length(); /** * @return content of this edit buffer */ public String toString(); }
-
一个非常简单的代表对这种数据类型将只是一个串:
public class SimpleBuffer implements EditBuffer { private String text; // Rep invariant: // true // Abstraction function: // represents the sequence text[0],...,text[text.length()-1]
-
不利的这种代表的是,每次我们做了一个插入或删除,我们必须复制整个串入一个新的串。 这变得昂贵。 另一个代表我们可以利用的将是一个字阵列,空间的结束。 那好,如果用户只需输入的新案文末尾的文件(我们不需要复制任何东西),但是如果使用者是打字开头的文件,然后我们在复制整个文件的每一按键。
一个更有趣的代表,这是由许多使用文字编辑在实践中,被称为一种 缓冲间隙 . 它基本上是一个字阵列出了额外的空间,但是代替所有额外的空间结束时,将额外的空间是一个 空隙 ,可以出现在任何地方的缓冲区。 每当插入或删除作需要做,本数据类型的第一个移动的间隙的位置的操作,随后不会插入或删除。 如果差距是已经存在,那么没有什么需要复制—插入刚消耗的一部分的间隙,并且删除,只是扩大的差距! 间隙的缓冲区是特别适合于表示一串正在编辑通过一个用户标,由于插入和删除倾向于将重点围绕标,所以对间隙很少移动。
/** GapBuffer is a non-threadsafe EditBuffer that is optimized * for editing with a cursor, which tends to make a sequence of * inserts and deletes at the same place in the buffer. */ public class GapBuffer implements EditBuffer { private char[] a; private int gapStart; private int gapLength; // Rep invariant: // 0 <= gapStart <= a.length // 0 <= gapLength <= a.length - gapStart // Abstraction function: // represents the sequence a[0],...,a[gapStart-1], // a[gapStart+gapLength],...,a[a.length-1]
在一个多用户的情况,我们希望多个缺口,每个用户的光标,但我们将使用一个单一的差距。
步骤来开发的数据类型
回想一下我们的食谱用于设计和执行一个安达泰:
- 指定。 定义的行动(签名方法和规格). 我们这样做的
EditBuffer
接口。 - 测试。 发展试验的情况下对操作。 看看
EditBufferTest
在所提供的编码。 测试包括一个试验战略,这一战略的基础是分区的参数空间行动。 - 。 选择一个代表。 我们选择了两个人
EditBuffer
,而这往往是一个很好的想法:- 实现一个简单、暴力rep第一次。 这是更易于编写,你就更有可能得到它的权利,并且它将验证你的测试的情况下和你说明书,所以你可以解决的问题在他们之前,你移动到更难实现。 这就是为什么我们实施
SimpleBuffer
之前移动到GapBuffer
. 不要扔掉你的简单的版本,保持它的周围,这样,你有什么测试和比较反对的情况下东西去了错误的更为复杂。 - 写下代表不变的和抽象的功能,并实施
checkRep()
.checkRep()
声称代表不变,在结束每一个构造、生产者,增变的方法。 (通常没有必要叫它在结束的一个观察员,因为该代表没有改变。) 事实上,断言可能会非常有用于测试的复杂的实现,所以它不是一个坏主意还断言的后置条件的末端的一个复杂的方法。 你会看到一个这样的例子中GapBuffer.moveGap()
在代码与这阅读。
- 实现一个简单、暴力rep第一次。 这是更易于编写,你就更有可能得到它的权利,并且它将验证你的测试的情况下和你说明书,所以你可以解决的问题在他们之前,你移动到更难实现。 这就是为什么我们实施
在所有这些步骤,我们的工作完全单的螺纹。 多线程的客户应该在我们的脑海中在所有时间的话,我们正在编写规格和选择的代表(我们稍后会看到的,小心选择的操作可能是必要的,以避免竞争条件在客户的数据类型). 但得到它的工作,并彻底的测试,在一个循序,单线的环境中第一次。
现在,我们已准备好进行下一步骤:
- 同步。 做一个说法,你的代表是线程安全的。 把它写下来明确地评论在你的班级,由代表不变,以便维护者知道你如何设计线安全进入类。
这一部分阅读是有关如何执行第4步。 我们已经看到了 如何使一线的安全参数 ,但是这时,我们将依靠同步,在这样的说法。
然后额外的步骤,我们暗示了以上:
- 迭代 . 你可能会发现你的选择的操作使得它很难编写一个线程安全类型的担保客户的需要。 你可能会发现这一步骤1,或者在第2步的时候你写测试,或者在步骤3或4时实施。 如果是这种情况下,返回和完善的设置的操作你的安达泰提供。
锁定
锁是这样常用的Java提供它们作为一个内置的语言功能。
在爪哇,每个对象锁定隐含地与它有关—一个 String
一阵,一个 ArrayList
和每一类的创建,所有他们的对象的实例具有一种锁。 即使是一个谦虚的 Object
有一个锁,使裸露的 Object
s经常用于明确锁定:
Object lock = new Object();
你不能打电话 acquire
和 release
Java固有的锁,但是。 而不是你使用 synchronized
声明获得锁期间的发言块:
synchronized (lock) { // thread blocks here until lock is free
// now this thread has the lock
balance = balance + 1;
// exiting the block releases the lock
}
同步区域提供这样的 相互排斥 :只有一个线程的时间可以同步区域的守护着一个给予对象是锁。 换句话说,你们回来的顺序编程的世界中,只有一个线运行的时间,至少相对于其他同步区域指相同的对象。
锁定卫对数据的访问
锁被用来 保护 一个共用的数据变量,如账户余额下所示。 如果所有访问一个数据变量的保护(包围,通过一个同步的方框)通过同样的锁定对象,那么这些访问将保证原子—不间断通过其他线。
锁只有提供相互排斥,与其他线,获得相同的锁。 所有访问一个数据变量,必须把守相同的锁。 你可能会保护的整个集合的变量后面一个单一的锁,但是,所有模块都必须同意在其上锁定他们都将获得与发布。
因为每一个目Java有一个锁隐含地与它相关联的,你可能认为拥有对象锁定会自动防止了其他线访问的对象。 那个不是这种情况。 当一个螺纹 t 取得的一个目的锁使用 synchronized (obj) { ... }
,它并一件事只有一件事:它可以防止的其他线进入他们自己的 synchronized(
表达 )
方框,其中 表达的 指的是同样的对象 obj
直到thread t 完成其同步块。 就是这样。 甚至在 t 是在其同步块,另外一个线程的可能危险地演变的目的,简单地忽略使用 synchronized
本身。 为了使用对象锁定为同步,你必须明确和精心保护每一个这样的访问有适当的 synchronized
阻止或方法的关键词。
监视器模式
当你写作的方法的一类,最方便的锁定对象本身的实例,即 this
. 作为一个简单的方法,我们可以保护整个共和国一类包装的所有访问的代表里面 synchronized (this)
.
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
...
public SimpleBuffer() {
synchronized (this) {
text = "";
checkRep();
}
}
public void insert(int pos, String ins) {
synchronized (this) {
text = text.substring(0, pos) + ins + text.substring(pos);
checkRep();
}
}
public void delete(int pos, int len) {
synchronized (this) {
text = text.substring(0, pos) + text.substring(pos+len);
checkRep();
}
}
public int length() {
synchronized (this) {
return text.length();
}
}
public String toString() {
synchronized (this) {
return text;
}
}
}
注意到非常仔细的纪律在这里。 每一个 方法,触及的代表必须守卫与锁甚至显然小型和微不足道的像 length()
和 toString()
. 这是因为读必须守卫以及写的—如果读留无人看守,那么他们可能看到rep在部分修改的状态。
这种方法被称为 监视器模式 . 监视器是一类,其方法是相互排斥的,所以只有一线可以是内部的一个实例类的时间。
Java提供了一些法糖这有助于监测模式。 如果添加的关键字 synchronized
一方法的签名,然后Java会采取行动,如果你写的 synchronized (this)
周围的方法主体。 这样的代码下面是一个等效的方式来实现同步 SimpleBuffer
:
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
...
public SimpleBuffer() {
text = "";
checkRep();
}
public synchronized void insert(int pos, String ins) {
text = text.substring(0, pos) + ins + text.substring(pos);
checkRep();
}
public synchronized void delete(int pos, int len) {
text = text.substring(0, pos) + text.substring(pos+len);
checkRep();
}
public synchronized int length() {
return text.length();
}
public synchronized String toString() {
return text;
}
}
注意到 SimpleBuffer
构造不能有一个 synchronized
关键词。 Java实际上禁止,在句法上的,因为一个目正在建设预计将局限于一个单一线,直到它已经返回,从其构造。 所以同步的构造应当是不必要的。 你仍然可以同步的一个构造包裹它的身体在 synchronized(this)
方框,如果你愿意的话。
在Java教程,读作:
阅读训练
与之同步的锁
这个名单是我的,我的
确定罚款,但这同步清单完全是我的
我听说你喜欢锁所以我获取你的锁所以你可以锁而你获得
线的安全参数同步
现在,我们在保护 SimpleBuffer
’s rep锁,我们可以写一线的安全参数:
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
// Rep invariant:
// true
// Abstraction function:
// represents the sequence text[0],...,text[text.length()-1]
// Safety from rep exposure:
// text is private and immutable
// Thread safety argument:
// all accesses to text happen within SimpleBuffer methods,
// which are all guarded by SimpleBuffer's lock
同样的论点工作 GapBuffer
如果我们使用监视器模式来同其所有的方法。
注意,封装的类,没有代表的曝光是非常重要,使这个参数。 如果案文:
public String text;
然后客户以外 SimpleBuffer
能够读和写,它不知道,他们应该首先获得锁, SimpleBuffer
将不再是线程安全的。
锁定律
一个锁定的纪律是一项战略,确保同步码线程安全的。 我们必须满足两个条件:
- 每一个共享的可变变必须把守的一些锁。 数据可能不会阅读或写,除了内部同步块获取,锁。
- 如果一个固定涉及多个共享的可变变量(这甚至可能在不同的对象),那么所有的变量的参与必须被看守的 相同的 锁。 一旦一个线程取得锁,不变的,必须重新建立之前释放锁。
监视器模式的作用在这里满足这两个规则。 所有共有的可变数据中的代表—其中的代表不变的,取决于是把守相同的锁。
原子操作
考虑找到和替换操作上的 EditBuffer
数据类型:
/** Modifies buf by replacing the first occurrence of s with t.
* If s not found in buf, then has no effect.
* @return true if and only if a replacement was made
*/
public static boolean findReplace(EditBuffer buf, String s, String t) {
int i = buf.toString().indexOf(s);
if (i == -1) {
return false;
}
buf.delete(i, s.length());
buf.insert(i, t);
return true;
}
这种方法使得三个不同的话 buf
—要把这转换为字符串中为了搜索 s
删除旧的文本,然后插入 t
在它的地方。 尽管这些呼吁独立是原子, findReplace
方法作为一个整体不是线程安全,因为其他线可能会改变缓冲区的同时 findReplace
是的工作,从而导致其删除的错误的地区,或者将替换回在错误的地方。
为了防止这种情况, findReplace
需要同步与所有其他客户 buf
.
给客户访问锁
它有时使你的数据类型的锁定可用于客户,以便他们可以利用它来实现更高水平的原子操作使用你的数据类型。
这样一种方法的问题 findReplace
是的文件,客户可以使用 EditBuffer
’s锁的同步:
/** An EditBuffer represents a threadsafe mutable string of characters
* in a text editor. Clients may synchronize with each other using the
* EditBuffer object itself. */
public interface EditBuffer {
...
}
然后 findReplace
可以同步 buf
:
public static boolean findReplace(EditBuffer buf, String s, String t) {
synchronized (buf) {
int i = buf.toString().indexOf(s);
if (i == -1) {
return false;
}
buf.delete(i, s.length());
buf.insert(i, t);
return true;
}
}
这样做的效果是扩大的同步区域的监测模式已经把周围的人 toString
, delete
, insert
方法,纳入一个单一的原子地区,以确保所有三个方法是执行而不干扰其他线。
无处不在
那是线的安全问题的投入 synchronized
关键词上的每个方法在你的程序? 不幸的是没有的。
第一,你真的不想以同步的方法,慎之又慎。 同步规定了一个大型的费用在你的节目。 让一个同步的方法的呼吁可能需要明显更长,因为需要获得锁(和冲缓存和与其他通信处理器的). Java留下许多其可变数据类型不同步的默认情况下完全出于这些性能的原因。 当你不需要同步,不要使用它。
另一种说法使用 synchronized
在一个更深思熟虑的方式是最大限度地减少的范围的访问,以你锁。 加入 synchronized
每一个方法意味着你的锁定对象本身,而每一个客户为对象的自动具有参考你的锁,它可以获得和释放。 你的线的安全机构因此是公开的,可以干预的客户。 相反,使用锁,是一个目内部的代表,以及获得适当的和谨慎使用 synchronized()
块。
最后,它实际上不足以撒 synchronized
无处不在。 滴 synchronized
到一个方法没有思想意味着你取一个锁没有考虑其它锁,或关于它是否是正确锁定对于保护共同数据访问你要做。 假设我们曾试图解决 findReplace
’s的同步问题简单地通过放弃 synchronized
在其声明:
public static synchronized boolean findReplace(EditBuffer buf, ...) {
这不会做我们想要什么。 这将确实获得锁—因为 findReplace
是一种静态的方法,它将获得一个静态锁对整个类 findReplace
发生,而不是一个实例对象锁。 结果,只有一线可以通话 findReplace
在一个时间—即使其他线想要操作上 不同的 缓冲区,这应该是安全的,他们还是会受阻,直到这个锁是免费的。 所以我们就会遭受重大损失的性能,因为只有一个用户的我们的大规模多用户编辑将被允许这样做寻找和替换的时间,即使他们是所有编辑不同的文件。
更糟糕,但是,它不会提供有益的保护,因为其他代码的接触到的文件可能不会获得同样的锁。 它不会实际上消除我们的竞争条件。
的 synchronized
关键字是不是灵丹妙药。 线的安全需要一个学科使用监禁、不可改变的,或者锁,以保护共享数据。 和纪律需要写下来,或者不知道它是什么。
设计一种数据类型的并发
findReplace
’s问题可以解释另一种方法: EditBuffer
接口真的不是友好的,多个同时进行的客户。 它依赖于整数指数的指定插入和删除个地点,它们极为脆弱的其它基因突变。 如果有人插入或删除之前的位置的索引,则该指标变得无效。
所以如果我们设计一种数据类型的专门用于一个并行系统,我们需要思考提供操作有更好的定义的语义学的时候他们是交织的。 例如,它可能会更好,对的 EditBuffer
与 Position
数据类型的代表的光标的位置在缓冲区,或者甚至一个 Selection
数据类型中代表一个选择的范围。 一旦获得, Position
可以保持其位置在案文中对洗的插入和删除围绕着它,直到客户准备使用, Position
. 如果其他一些线删除所有周围的文本 Position
,然后 Position
能够告知随后的客户有关发生了什么事(也许有一个例外),以及允许客户以决定做什么。 这些种类的考虑因素来发挥作用的时候设计一个数据类型为发。
作为另一个例子,请考虑 ConcurrentMap
接口。 这个接口现有的延伸 Map
接口,添加一些关键方法,这通常需要为原子操作上的一个共享的可变地图,例如:
map.putIfAbsent(key,value)
是原子版本的
if ( ! map.containsKey(key)) map.put(key, value);
map.replace(key, value)
是原子版本的
if (map.containsKey(key)) map.put(key, value);
僵局抬头
锁定的方法给线安全是强大的,但是(不同于监禁和不可变性)介绍了阻止进入程序。 线有时必须等待其他线获得出同步区域之前,他们可以继续进行。 并阻断提高的可能性僵局。
正如我们看到上面 ,僵持发生在线获得多个锁的同时,两个线程结束封锁,同时保持锁定他们是每个正在等待对其他释放。 监视器模式不幸的是,使这个相当容易做到的。 这是一个例子。
假设我们的建模的社会网络的一系列书籍:
public class Wizard {
private final String name;
private final Set<Wizard> friends;
// Rep invariant:
// friend links are bidirectional:
// for all f in friends, f.friends contains this
// Concurrency argument:
// threadsafe by monitor pattern: all accesses to rep
// are guarded by this object's lock
public Wizard(String name) {
this.name = name;
this.friends = new HashSet<Wizard>();
}
public synchronized boolean isFriendsWith(Wizard that) {
return this.friends.contains(that);
}
public synchronized void friend(Wizard that) {
if (friends.add(that)) {
that.friend(this);
}
}
public synchronized void defriend(Wizard that) {
if (friends.remove(that)) {
that.defriend(this);
}
}
}
如Facebook,这种社会网络是双向的:如果 x 是朋友, y ,然后 y 是朋友 x . 的 friend()
和 defriend()
方法强制执行,不变,修改了该代表的这两种对象,因为它们使用监视器模式意味着获取锁,这两个对象。
让我们创造一对夫妇的向导:
Wizard harry = new Wizard("Harry Potter");
Wizard snape = new Wizard("Severus Snape");
然后想想会发生什么当两个独立的线多次运行:
// thread A // thread B
harry.friend(snape); snape.friend(harry);
harry.defriend(snape); snape.defriend(harry);
我们将僵局,非常迅速。 这里就是为什么。 想纹一个是关于执行 harry.friend(snape)
和线B是关于执行 snape.friend(harry)
.
- 螺纹一个获取该锁上
harry
(因为朋友的方法是同步)。 - 然后线B取得的锁上
snape
(出于同样的原因). - 他们两个更新他们个人代表独立,然后试图呼叫
friend()
在其他对象,这要求他们获得锁上的其他对象。
因此,一个持哈里,等待内普和B持斯内普及等待哈利。 两个线程是卡在 friend()
,所以没有一个曾经将管理退出同步区域和释放锁。 这是一个典型的致命的怀抱。 该计划只是停止。
问题的实质是获取多个锁,并保持一些锁定在等待另一个锁成为免费的。
注意到它是可能的线和线B的交叉这种僵局不会发生:也许螺纹一个获取和释放都锁之前线B有足够的时间获取的第一个。 如果锁定参与在一僵局也参与在竞争条件—并且很多时候,他们的—那僵局将只是因为很难再现,或者调试。
僵局的解决方案1:锁定货
一种方法防止的僵局是把排序上的锁,这需要同时获取,并确保所有代码获取锁在这个顺序。
在我们的社会网络的实例,我们可能总是取得锁上 Wizard
目的按字母顺序排列的向导的姓名。 由于线一线和B都要锁对哈里和斯内普,他们将获得它们在这一顺序:哈利锁,然后再斯内普。 如果螺的一个得到哈里的锁之前,B会,它也将获得斯内普是锁之前,B,因为B不能继续进行,直到一个版本哈利锁。 订单上的锁部队的一个排序在线获取他们,所以没有办法生产周期在等待的图表。
这是什么代码看起来可能会像:
public void friend(Wizard that) {
Wizard first, second;
if (this.name.compareTo(that.name) < 0) {
first = this; second = that;
} else {
first = that; second = this;
}
synchronized (first) {
synchronized (second) {
if (friends.add(that)) {
that.friend(this);
}
}
}
}
(注意,决定以锁定按字母顺序通过的人的名字会工作的现的情况的本哈利波特书,但它不会在现实生活中的社会网络。 为什么不呢? 什么会更好地用于锁定货比的名字?)
虽然锁订是有用的(特别是在这样的代码操作系统内核的),它有一些缺陷,在实践中。
- 第一,这不是模块式的代码已经知道所有的门锁系统,或至少在其子系统。
- 第二,它可能难以或无法代码确切知道哪些锁定它将需要之前,甚至获得第一个。 它可能需要做一些计算。 想做一个深入的先搜索的社会网络图表,例如—你怎么知道哪些节点需要被锁定,以前你甚至开始寻找他们吗?
僵局的解决方案2:粗粒度锁定
更常用的方法比锁订购,特别是用于应用程序(而不是操作系统或设备司机的编程),是使用粗糙的锁定使用一个单一的锁,以保护许多对象的情况下,或者甚至整个子系统的程序。
例如,我们可有一个单一的锁定对于整个社会网络,并且拥有所有操作上的任何其组成部分同步上锁。 在代码下面,所有的 Wizard
s属于一个 Castle
我们只是用, Castle
对象锁定同步:
public class Wizard {
private final Castle castle;
private final String name;
private final Set<Wizard> friends;
...
public void friend(Wizard that) {
synchronized (castle) {
if (this.friends.add(that)) {
that.friend(this);
}
}
}
}
粗粒的锁可以有一个显着的表现的刑罚。 如果你在保护一大堆的可变数据与一个单一的锁,然后你放弃的能力,访问任何数据同时进行。 在最坏的情况下,具有一个单一的保护锁一切,你的节目可能基本上按顺序—只有一个线程被允许取得进展的时间。
阅读训练
僵局
锁定了
在实践并发
目标
现在是一个很好的时间不到一个级别,并看看我们在做什么。 回想一下,我们的主要目标是创建软件安全,从错误的,容易理解,并为改变做好准备。
建立并发软件显然是一个挑战,为所有这三个目标。 我们可以打破的问题,为两大类。 当我们询问是否有并发程序 的安全,从错误 ,我们关心的两个特性:
- 安全。 不会的并行程序满足其不变量及其规格吗? 比赛在访问的可变数据威胁的安全。 安全问一个问题:你可以证明 一些不好的事情从未发生过 ?
- 活动. 不会的程序继续运行,并最终做你想要什么,或者它卡住的地方永远等待的事件,将永远不会发生? 你可以证明 一些良好的事情最终会发生 ?
僵局的威胁活动. 活动还可能需要 公平 ,这意味着,并发的模块都给予处理的能力方面取得进展他们的计算。 公平性的主要问题的操作系统的线调度程序,但可以影响(或好或坏)通过设置线的优先事项。
战略
什么战略通常遵循的现实的程序?
- 库的数据结构 或者使用不同步(要提高性能,单线的客户,同时保留它的多线程的客户增加锁上)或监视器模式。
- 可变数据的结构与许多地区 通常使用粗粒度锁定或螺纹监禁。 最图形用户接口工具包,按照一些方法,因为一个图形用户接口基本上是一个很大的可变树的可变对象。 Java摇摆,图形用户接口工具包,使用螺纹监禁。 只有一个单一专线是允许接入摇摆的树。 其他的线必须通过消息得专用线程,以便访问的树。
- 搜索 常常使用不可改变的数据类型。 我们的 布尔公式可满足性的搜索 将容易使多线程,因为所有的数据类型的参与是不可改变的。 就不会有风险,不论种族或死锁。
- 操作系统 常常使用细锁定为了得到高性能和使用锁定排序以处理僵局的问题。
我们省略了一个重要方式共享数据的可变,因为它的范围之外这个过程中,但它值得一提的是: 一个数据库 . 数据库系统被广泛用于分发客户/服务器的系统(如网应用程序。 数据库,避免竞争条件下使用 的交易 ,它们是类似的同步区域,其影响是原子,但他们没有获得锁,虽然交易可能会失败,和可以回滚如果事实证明,一个种族的发生。 数据库还可以管理锁,并处理锁定为自动。 欲了解更多有关如何使用数据库系统的设计,6.170软件的工作室是强烈建议;为更多关于如何数据库,在里面工作,采取6.814数据库系统。
如果你有兴趣的 表现 并发计划—因为业绩是通常的原因之一,我们增加并发一个系统在第一个地方—然后6.172性能的工程是针对你的课程。
摘要
生产并发程序的安全,从错误的,容易理解,并为改变做好准备需要仔细的思考。 Heisenbugs会飞掠走,因为只要你尝试销他们下来,所以调试只是不是一种有效的方式来实现正确的线程安全代码。 和线可以交他们的行动在许多不同的方式,你永远不可能测试,甚至一小部分的所有可能的处决。
- 让线的安全参数对你的数据类型,文件在他们的代码。
- 获取锁定允许一个线程具有独特的数据的访问看守,锁,迫使其他线块—只要这些螺纹也试图获得相同的锁。
- 的 监视器模式 警卫的代表的数据类型与一个单一的锁,获取的每一个方法。
,因为它的范围之外这个过程中,但它值得一提的是: 一个数据库 . 数据库系统被广泛用于分发客户/服务器的系统(如网应用程序。 数据库,避免竞争条件下使用 的交易 ,它们是类似的同步区域,其影响是原子,但他们没有获得锁,虽然交易可能会失败,和可以回滚如果事实证明,一个种族的发生。 数据库还可以管理锁,并处理锁定为自动。 欲了解更多有关如何使用数据库系统的设计,6.170软件的工作室是强烈建议;为更多关于如何数据库,在里面工作,采取6.814数据库系统。
如果你有兴趣的 表现 并发计划—因为业绩是通常的原因之一,我们增加并发一个系统在第一个地方—然后6.172性能的工程是针对你的课程。
摘要
生产并发程序的安全,从错误的,容易理解,并为改变做好准备需要仔细的思考。 Heisenbugs会飞掠走,因为只要你尝试销他们下来,所以调试只是不是一种有效的方式来实现正确的线程安全代码。 和线可以交他们的行动在许多不同的方式,你永远不可能测试,甚至一小部分的所有可能的处决。
- 让线的安全参数对你的数据类型,文件在他们的代码。
- 获取锁定允许一个线程具有独特的数据的访问看守,锁,迫使其他线块—只要这些螺纹也试图获得相同的锁。
- 的 监视器模式 警卫的代表的数据类型与一个单一的锁,获取的每一个方法。
- 阻塞引起的获取多个锁造成的可能的僵局。