- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
14.2.2 Java 如何共享资源
对一种特殊的资源——对象中的内存——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() 的例子显示出这样做是很不通的)。但它也揭示出获取一把锁并非一种“廉价”方案——为一次方法调用付出的代价(进入和退出方法,不执行方法主体)至少要累加到四倍,而且根据我们的具体现方案,这一代价还有可能变得更高。所以假如已知一个方法不会造成冲突,最明智的做法便是撤消其中的 synchronized 关键字。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论