📄 如果我是国王:关于解决 java 编程语言线程问题的建议.htm
字号:
}
}
}
void b()
{ synchronized( lock2 )
{ synchronized( lock1 )
{ // do something
}
}
}
</PRE></TD></TR></TBODY></TABLE>
<P>设想一个线程调用 <CODE>a()</CODE>,但在获得 <CODE>lock1</CODE>之后在获得
<CODE>lock2</CODE> 之前被剥夺运行权。 第二个线程进入运行,调用 <CODE>b()</CODE>,获得了
<CODE>lock2</CODE>,但是由于第一个线程占用 <CODE>lock1</CODE>,所以它无法获得
<CODE>lock1</CODE>,所以它随后处于等待状态。此时第一个线程被唤醒,它试图获得
<CODE>lock2</CODE>,但是由于被第二个线程占据,所以无法获得。此时出现死锁。下面的
synchronize-on-multiple-objects 的语法可解决这个问题:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE> //...
void a()
{ synchronized( lock1 && lock2 )
{
}
}
void b()
{ synchronized( lock2 && lock3 )
{
}
}
</PRE></TD></TR></TBODY></TABLE>
<P>编译器(或虚拟机)会重新排列请求锁的顺序,使 <CODE>lock1</CODE> 总是被首先获得,这就消除了死锁。</P>
<P>但是,这种方法对多线程不一定总成功,所以得提供一些方法来自动打破死锁。一个简单的办法就是在等待第二个锁时常释放已获得的锁。这就是说,应采取如下的等待方式,而不是永远等待:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE> while( true )
{ try
{ synchronized( some_lock )[10]
{ // do the work here.
break;
}
}
catch( TimeoutException e )
{ continue;
}
}
</PRE></TD></TR></TBODY></TABLE>
<P>如果等待锁的每个程序使用不同的超时值,就可打破死锁而其中一个线程就可运行。我建议用以下的语法来取代前面的代码:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE> synchronized( some_lock )[]
{ // do the work here.
}
</PRE></TD></TR></TBODY></TABLE>
<P><CODE>synchronized</CODE>
语句将永远等待,但是它时常会放弃已获得的锁以打破潜在的死锁可能。在理想情况下,每个重复等待的超时值比前一个相差一随机值。</P><A
id=3 name=3></A>
<P><STRONG class=subhead>改进 <CODE>wait()</CODE> 和
<CODE>notify()</CODE></STRONG><BR><CODE>wait()</CODE>/<CODE>notify()</CODE>
系统也有一些问题:</P>
<UL>
<LI>无法检测 <CODE>wait()</CODE> 是正常返回还是因超时返回。
<LI>无法使用传统条件变量来实现处于一个“信号”(signaled)状态。
<LI>太容易发生嵌套的监控(monitor)锁定。 </LI></UL>
<P>超时检测问题可以通过重新定义 <CODE>wait()</CODE> 使它返回一个 <CODE>boolean</CODE> 变量
(而不是 <CODE>void</CODE> ) 来解决。一个 <CODE>true</CODE> 返回值指示一个正常返回,而
<CODE>false</CODE> 指示因超时返回。</P>
<P>基于状态的条件变量的概念是很重要的。如果此变量被设置成 <CODE>false</CODE>
状态,那么等待的线程将要被阻断,直到此变量进入 <CODE>true</CODE> 状态;任何等待 <CODE>true</CODE>
的条件变量的等待线程会被自动释放。 (在这种情况下,<CODE>wait()</CODE> 调用不会发生阻断。)。通过如下扩展
<CODE>notify()</CODE> 的语法,可以支持这个功能:</P>
<TABLE cellSpacing=0 cellPadding=4 border=1>
<TBODY>
<TR>
<TD align=left width="20%">notify();</TD>
<TD align=left>释放所有等待的线程,而不改变其下面的条件变量的状态。</TD></TR>
<TR>
<TD align=left width="20%">notify(true);</TD>
<TD align=left>把条件变量的状态设置为 true 并释放任何等待的进程。其后对于
<CODE>wait()</CODE> 的调用不会发生阻断。</TD></TR>
<TR>
<TD align=left width="20%">notify(false);</TD>
<TD align=left>把条件变量的状态设置为 false (其后对于 <CODE>wait()</CODE>
的调用会发生阻断)。</TD></TR></TBODY></TABLE>
<P>嵌套监控锁定问题非常麻烦,我并没有简单的解决办法。嵌套监控锁定是一种死锁形式,当某个锁的占有线程在挂起其自身之前不释放锁时,会发生这种嵌套监控封锁。下面是此问题的一个例子(还是假设的),但是实际的例子是非常多的:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>class Stack
{
LinkedList list = new LinkedList();
public synchronized void push(Object x)
{ synchronized(list)
{ list.addLast( x );
notify();
}
}
public synchronized Object pop()
{ synchronized(list)
{ if( list.size() <= 0 )
wait();
return list.removeLast();
}
}
}
</PRE></TD></TR></TBODY></TABLE>
<P>此例中,在 <CODE>get()</CODE> 和 <CODE>put()</CODE> 操作中涉及两个锁:一个在
<CODE>Stack</CODE> 对象上,另一个在 <CODE>LinkedList</CODE>
对象上。下面我们考虑当一个线程试图调用一个空栈的 pop() 操作时的情况。此线程获得这两个锁,然后调用
<CODE>wait()</CODE> 释放 <CODE>Stack</CODE> 对象上 的锁,但是没有释放在 list
上的锁。如果此时第二个线程试图向堆栈中压入一个对象,它会在 <CODE>synchronized(list)</CODE>
语句上永远挂起,而且永远不会被允许压入一个对象。由于第一个线程等待的是一个非空栈,这样就会发生死锁。这就是说,第一个线程永远无法从
<CODE>wait()</CODE> 返回,因为由于它占据着锁,而导致第二个线程永远无法运行到
<CODE>notify()</CODE> 语句。</P>
<P>在这个例子中,有很多明显的办法来解决问题:例如,对任何的方法都使用同步。但是在真实世界中,解决方法通常不是这么简单。</P>
<P>一个可行的方法是,在 <CODE>wait()</CODE>
中按照反顺序释放当前线程获取的<I>所有</I>锁,然后当等待条件满足后,重新按原始获取顺序取得它们。但是,我能想象出利用这种方式的代码对于人们来说简直无法理解,所以我认为它不是一个真正可行的方法。如果您有好的方法,请给我发
e-mail。</P>
<P>我也希望能等到下述复杂条件被实现的一天。例如:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>(a && (b || c)).wait();
</PRE></TD></TR></TBODY></TABLE>
<P>其中 <CODE>a</CODE>、<CODE>b</CODE> 和 <CODE>c</CODE> 是任意对象。</P><A
id=4 name=4></A>
<P><STRONG class=subhead>修改 <CODE>Thread</CODE>
类</STRONG><BR>同时支持抢占式和协作式线程的能力在某些服务器应用程序中是基本要求,尤其是在想使系统达到最高性能的情况下。我认为
Java 编程语言在简化线程模型上走得太远了,并且 Java 编程语言应支持 Posix/Solaris
的“绿色(green)线程”和“轻便(lightweight)进程”概念(在“(<I>Taming Java
Threads</I>”第一章中讨论)。 这就是说,有些 Java 虚拟机的实现(例如在 NT 上的 Java
虚拟机)应在其内部仿真协作式进程,其它 Java 虚拟机应仿真抢占式线程。而且向 Java 虚拟机加入这些扩展是很容易的。</P>
<P>一个 Java 的 <CODE>Thread</CODE> 应始终是抢占式的。这就是说,一个 Java 编程语言的线程应像
Solaris 的轻便进程一样工作。 <CODE>Runnable</CODE> 接口可以用于定义一个 Solaris
式的“绿色线程”,此线程必需能把控制权转给运行在相同轻便进程中的其它绿色线程。</P>
<P>例如,目前的语法:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE><A id=My_thread name=My_thread> class My_thread implements Runnable</A>
<A id=My_thread.run() name=My_thread.run()> { public void run(){ /*...*/ }</A>
}
new Thread( new My_thread );
</PRE></TD></TR></TBODY></TABLE>
<P>能有效地为 <CODE>Runnable</CODE> 对象产生一个绿色线程,并把它绑定到由
<CODE>Thread</CODE> 对象代表的轻便进程中。这种实现对于现有代码是透明的,因为它的有效性和现有的完全一样。</P>
<P>把 <CODE>Runnable</CODE> 对象想成为绿色线程,使用这种方法,只需向 <CODE>Thread</CODE>
的构造函数传递几个 <CODE>Runnable</CODE>对象,就可以扩展 Java
编程语言的现有语法,以支持在一个单一轻便线程有多个绿色线程。(绿色线程之间可以相互协作,但是它们可被运行在其它轻便进程
(<CODE>Thread</CODE> 对象) 上的绿色进程(<CODE>Runnable</CODE> 对象)
抢占。)。例如,下面的代码会为每个 runnable 对象创建一个绿色线程,这些绿色线程会共享由 <CODE>Thread</CODE>
对象代表的轻便进程。</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>new Thread( new My_runnable_object(), new My_other_runnable_object() );
</PRE></TD></TR></TBODY></TABLE>
<P>现有的覆盖(override) <CODE>Thread</CODE> 对象并实现 <CODE>run()</CODE>
的习惯继续有效,但是它应映射到一个被绑定到一轻便进程的绿色线程。(在 <CODE>Thread()</CODE> 类中的缺省
<CODE>run()</CODE> 方法会在内部有效地创建第二个 <CODE>Runnable</CODE> 对象。)</P><A
id=5 name=5></A>
<P><STRONG
class=subhead>线程间的协作</STRONG><BR>应在语言中加入更多的功能以支持线程间的相互通信。目前,<CODE>PipedInputStream</CODE>
和 <CODE>PipedOutputStream</CODE> 类可用于这个目的。但是对于大多数应用程序,它们太弱了。我建议向
<CODE>Thread</CODE> 类加入下列函数:</P>
<OL>
<LI>增加一个 <CODE>wait_for_start()</CODE> 方法,它通常处于阻塞状态,直到一个线程的
<CODE>run()</CODE> 方法启动。(如果等待的线程在调用 run
之前被释放,这没有什么问题)。用这种方法,一个线程可以创建一个或多个辅助线程,并保证在创建线程继续执行操作之前,这些辅助线程会处于运行状态。
<LI>(向 <CODE>Object</CODE> 类)增加 <CODE>$send (Object o)</CODE> 和
<CODE>Object=$receive()</CODE>
方法,它们将使用一个内部阻断队列在线程之间传送对象。阻断队列应作为第一个 <CODE>$send()</CODE>
调用的副产品被自动创建。 <CODE>$send()</CODE> 调用会把对象加入队列。
<CODE>$receive()</CODE>
调用通常处于阻塞状态,直到有一个对象被加入队列,然后它返回此对象。这种方法中的变量应支持设定入队和出队的操作超时能力:
<CODE>$send (Object o, long timeout)</CODE> 和 <CODE>$receive (long
timeout)。</CODE> </LI></OL><A id=6 name=6></A>
<P><STRONG class=subhead>对于读写锁的内部支持</STRONG><BR>读写锁的概念应内置到 Java
编程语言中。读写器锁在“<I>Taming Java
Threads</I>”(和其它地方)中有详细讨论,概括地说:一个读写锁支持多个线程同时访问一个对象,但是在同一时刻只有一个线程可以修改此对象,并且在访问进行时不能修改。读写锁的语法可以借用
<CODE>synchronized</CODE> <CODE></CODE>关键字:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -