返回介绍

14.2.2 Java 如何共享资源

发布于 2024-10-15 23:56:32 字数 6031 浏览 0 评论 0 收藏 0

对一种特殊的资源——对象中的内存——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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文