对一种特殊的资源――对象中的内存――java提供了内建的机制来防止它们的冲突。由于我们通常将数据元素设为从属于private(私有)类,然后只通过方法访问那些内存,所以只需将一个特定的方法设为synchronized(同步的),便可有效地防止冲突。在任何时刻,只可有一个线程调用特定对象的一个synchronized方法(尽管那个线程可以调用多个对象的同步方法)。下面列出简单的synchronized方法:
synchronized void f() { /* ... */ }
synchronized void g() { /* ... */ }
每个对象都包含了一把锁(也叫作“监视器”),它自动成为对象的一部分(不必为此写任何特殊的代码)。调用任何synchronized方法时,对象就会被锁定,不可再调用那个对象的其他任何synchronized方法,除非第一个方法完成了自己的工作,并解除锁定。在上面的例子中,如果为一个对象调用f(),便不能再为同样的对象调用g(),除非f()完成并解除锁定。因此,一个特定对象的所有synchronized方法都共享着一把锁,而且这把锁能防止多个方法对通用内存同时进行写操作(比如同时有多个线程)。
每个类也有自己的一把锁(作为类的class对象的一部分),所以synchronized static方法可在一个类的范围内被相互间锁定起来,防止与static数据的接触。
注意如果想保护其他某些资源不被多个线程同时访问,可以强制通过synchronized方访问那些资源。
1. 计数器的同步
装备了这个新关键字后,我们能够采取的方案就更灵活了:可以只为twocounter中的方法简单地使用synchronized关键字。下面这个例子是对前例的改版,其中加入了新的关键字:
//: sharing2.java
// using the synchronized keyword to prevent
// multiple access to a particular resource.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class twocounter2 extends thread {
private boolean started = false;
private textfield
t1 = new textfield(5),
t2 = new textfield(5);
private label l =
new label("count1 == count2");
private int count1 = 0, count2 = 0;
public twocounter2(container c) {
panel p = new panel();
p.add(t1);
p.add(t2);
p.add(l);
c.add(p);
}
public void start() {
if(!started) {
started = true;
super.start();
}
}
public synchronized void run() {
while (true) {
t1.settext(integer.tostring(count1++));
t2.settext(integer.tostring(count2++));
try {
sleep(500);
} catch (interruptedexception e){}
}
}
public synchronized void synchtest() {
sharing2.incrementaccess();
if(count1 != count2)
l.settext("unsynched");
}
}
class watcher2 extends thread {
private sharing2 p;
public watcher2(sharing2 p) {
this.p = p;
start();
}
public void run() {
while(true) {
for(int i = 0; i < p.s.length; i++)
p.s[i].synchtest();
try {
sleep(500);
} catch (interruptedexception e){}
}
}
}
public class sharing2 extends applet {
twocounter2[] s;
private static int accesscount = 0;
private static textfield acount =
new textfield("0", 10);
public static void incrementaccess() {
accesscount++;
acount.settext(integer.tostring(accesscount));
}
private button
start = new button("start"),
observer = new button("observe");
private boolean isapplet = true;
private int numcounters = 0;
private int numobservers = 0;
public void init() {
if(isapplet) {
numcounters =
integer.parseint(getparameter("size"));
numobservers =
integer.parseint(
getparameter("observers"));
}
s = new twocounter2[numcounters];
for(int i = 0; i < s.length; i++)
s[i] = new twocounter2(this);
panel p = new panel();
start.addactionlistener(new startl());
p.add(start);
observer.addactionlistener(new observerl());
p.add(observer);
p.add(new label("access count"));
p.add(acount);
add(p);
}
class startl implements actionlistener {
public void actionperformed(actionevent e) {
for(int i = 0; i < s.length; i++)
s[i].start();
}
}
class observerl implements actionlistener {
public void actionperformed(actionevent e) {
for(int i = 0; i < numobservers; i++)
new watcher2(sharing2.this);
}
}
public static void main(string[] args) {
sharing2 applet = new sharing2();
// this isn't an applet, so set the flag and
// produce the parameter values from args:
applet.isapplet = false;
applet.numcounters =
(args.length == 0 ? 5 :
integer.parseint(args[0]));
applet.numobservers =
(args.length < 2 ? 5 :
integer.parseint(args[1]));
frame aframe = new frame("sharing2");
aframe.addwindowlistener(
new windowadapter() {
public void windowclosing(windowevent e){
system.exit(0);
}
});
aframe.add(applet, borderlayout.center);
aframe.setsize(350, applet.numcounters *100);
applet.init();
applet.start();
aframe.setvisible(true);
}
}
我们注意到无论run()还是synchtest()都是“同步的”。如果只同步其中的一个方法,那么另一个就可以自由忽视对象的锁定,并可无碍地调用。所以必须记住一个重要的规则:对于访问某个关键共享资源的所有方法,都必须把它们设为synchronized,否则就不能正常地工作。
现在又遇到了一个新问题。watcher2永远都不能看到正在进行的事情,因为整个run()方法已设为“同步”。而且由于肯定要为每个对象运行run(),所以锁永远不能打开,而synchtest()永远不会得到调用。之所以能看到这一结果,是因为accesscount根本没有变化。
为解决这个问题,我们能采取的一个办法是只将run()中的一部分代码隔离出来。想用这个办法隔离出来的那部分代码叫作“关键区域”,而且要用不同的方式来使用synchronized关键字,以设置一个关键区域。java通过“同步块”提供对关键区域的支持;这一次,我们用synchronized关键字指出对象的锁用于对其中封闭的代码进行同步。如下所示:
synchronized(syncobject) {
// this code can be accessed by only
// one thread at a time, assuming all
// threads respect syncobject's lock
}
在能进入同步块之前,必须在synchobject上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必须等候那把锁被释放。
可从整个run()中删除synchronized关键字,换成用一个同步块包围两个关键行,从而完成对sharing2例子的修改。但什么对象应作为锁来使用呢?那个对象已由synchtest()标记出来了――也就是当前对象(this)!所以修改过的run()方法象下面这个样子:
public void run() {
while (true) {
synchronized(this) {
t1.settext(integer.tostring(count1++));
t2.settext(integer.tostring(count2++));
}
try {
sleep(500);
} catch (interruptedexception e){}
}
}
这是必须对sharing2.java作出的唯一修改,我们会看到尽管两个计数器永远不会脱离同步(取决于允许watcher什么时候检查它们),但在run()执行期间,仍然向watcher提供了足够的访问权限。
当然,所有同步都取决于程序员是否勤奋:要访问共享资源的每一部分代码都必须封装到一个适当的同步块里。
2. 同步的效率
由于要为同样的数据编写两个方法,所以无论如何都不会给人留下效率很高的印象。看来似乎更好的一种做法是将所有方法都设为自动同步,并完全消除synchronized关键字(当然,含有synchronized run()的例子显示出这样做是很不通的)。但它也揭示出获取一把锁并非一种“廉价”方案――为一次方法调用付出的代价(进入和退出方法,
闽公网安备 35060202000074号