您的位置:首页 > 编程语言 > Java开发

61.JAVA编程思想——共享有限资源

2016-05-08 21:16 489 查看
61.JAVA编程思想——共享有限资源
可将单线程程序想象成一种孤立的实体,它能遍历我们的问题空间,而且一次只能做一件事情。由于只有一个实体,所以永远不必担心会有两个实体同时试图使用相同的资源,就象两个人同时都想停到一个车位,同时都想通过一扇门,甚至同时发话。

进入多线程环境后,它们则再也不是孤立的。可能会有两个甚至更多的线程试图同时同一个有限的资源。必须对这种潜在资源冲突进行预防,否则就可能发生两个线程同时访问一个银行帐号,打印到同一台计算机,以及对同一个值进行调整等等。

1     资源访问的错误方法

现在考虑换成另一种方式来使用频繁见到的计数器。在下面的例子中,每个线程都包含了两个计数器,它们在run()里增值以及显示。除此以外,我们使用了Watcher 类的另一个线程。它的作用是监视计数器,检查它们是否保持相等。这表面是一项无意义的行动,因为如果查看代码,就会发现计数器肯定是相同的。但实际情况却不一定如此。下面是程序的第一个版本:

1.1     代码

import java.awt.*;

import java.awt.event.*;

import java.applet.*;

class TwoCounter
extends Thread {

    private
boolean
started=
false;

    private TextField
t1= newTextField(5),
t2= newTextField(5);

    private Label
l = new Label("count1 == count2");

    private
int
count1= 0,
count2= 0;

    // Add thedisplay components as a panel

    // to thegiven container:

    public TwoCounter(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
void
run() {

        while (true) {

            t1.setText(Integer.toString(count1++));

            t2.setText(Integer.toString(count2++));

            try {

                sleep(500);

            } catch (InterruptedException
e) {

            }

        }

    }

    public
void
synchTest() {

        Sharing1.incrementAccess();

        if (count1 !=
count2)

            l.setText("Unsynched");

    }

}

class Watcher
extends Thread {

    private Sharing1
p;

    public Watcher(Sharing1
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
Sharing1 extends Applet {

    TwoCounter[] s;

    private
staticint
accessCount= 0;

    private
static
TextField aCount=
newTextField("0",10);

    public
staticvoid
incrementAccess() {

        accessCount++;

        aCount.setText(Integer.toString(accessCount));

    }

    private Button
start= newButton("Start"),observer=
newButton("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
TwoCounter[numCounters];

        for (int
i = 0; i <
s.length; i++)

            s[i] =
newTwoCounter(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 Watcher(Sharing1.this);

        }

    }

    public
staticvoid
main(String[]
args){

        Sharing1 applet =
new Sharing1();

        // 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("Sharing1");

        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);

    }

} /// :~

和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在TwoCounter 构建器加入Container。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以start()可能被多次调用。但对一个线程来说,对Thread.start()的多次调用是非法的(会产生违例)。在started 标记和过载的start()方法中,大家可看到针对这一情况采取的防范措施。

在run()中,count1 和count2的增值与显示方式表面上似乎能保持它们完全一致。随后会调用sleep();若没有这个调用,程序便会出错,因为那会造成CPU 难于交换任务。

synchTest()方法采取的似乎是没有意义的行动,它检查count1 是否等于count2;如果不等,就把标签设为“Unsynched”(不同步)。但是首先,它调用的是类Sharing1 的一个静态成员,以便增值和显示一个访问计数器,指出这种检查已成功进行了多少次(这样做的理由会在本例的其他版本中变得非常明显)。

Watcher 类是一个线程,它的作用是为处于活动状态的所有TwoCounter 对象都调用synchTest()。其间,它会对Sharing1 对象中容纳的数组进行遍历。可将Watcher 想象成它掠过TwoCounter 对象的肩膀不断地“偷看”。

Sharing1 包含了TwoCounter 对象的一个数组,它通过init()进行初始化,并在我们按下“start”按钮后作为线程启动。以后若按下“Observe”(观察)按钮,就会创建一个或者多个观察器,并对毫不设防的TwoCounter 进行调查。

下面才是最让人“不可思议”的。在TwoCounter.run()中,无限循环只是不断地重复相邻的行:

t1.setText(Integer.toString(count1++));

t2.setText(Integer.toString(count2++));

(和“睡眠”一样,不过在这里并不重要)。但在程序运行的时候,你会发现count1和count2 被“观察”(用Watcher 观察)的次数是不相等的!这是由线程的本质造成的——它们可在任何时候挂起(暂停)。所以在上述两行的执行时刻之间,有时会出现执行暂停现象。同时,Watcher 线程也正好跟随着进来,并正好在这个时候进行比较,造成计数器出现不相等的情况。

本例揭示了使用线程时一个非常基本的问题。我们跟无从知道一个线程什么时候运行。想象自己坐在一张桌子前面,桌上放有一把叉子,准备叉起自己的最后一块食物。当叉子要碰到食物时,食物却突然消失了(因为这个线程已被挂起,同时另一个线程进来“偷”走了食物)。这便是我们要解决的问题。有的时候,我们并不介意一个资源在尝试使用它的时候是否正被访问(食物在另一些盘子里)。但为了让多线程机制能够正常运转,需要采取一些措施来防止两个线程访问相同的资源——至少在关键的时期。

为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程会其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。如果车子的前座是有限的资源,高喊“这是我的!”的孩子会主张把它锁起来。

2     J a v a 如何共享资源

对一种特殊的资源——对象中的内存,Java 提供了内建的机制来防止它们的冲突。由于我们通常将数据元素设为从属于private(私有)类,然后只通过方法访问那些内存,所以只需将一个特定的方法设为synchronized(同步的),便可有效地防止冲突。在任何时刻,只可有一个线程调用特定对象的一个synchronized 方法(尽管那个线程可以调用多个对象的同步方法)。

下面列出简单的synchronized 方法:

synchronized void f() { /* ...*/ }

synchronized void g() { /* ...*/ }

每个对象都包含了一把锁(也叫作“监视器”),它自动成为对象的一部分(不必为此写任何特殊的代码)。调用任何synchronized 方法时,对象就会被锁定,不可再调用那个对象的其他任何synchronized方法,除非第一个方法完成了自己的工作,并解除锁定。在上面的例子中,如果为一个对象调用f(),便不能再为同样的对象调用g(),除非f()完成并解除锁定。因此,一个特定对象的所有synchronized 方法都共享着一把锁,而且这把锁能防止多个方法对通用内存同时进行写操作(比如同时有多个线程)。

每个类也有自己的一把锁(作为类的Class 对象的一部分),所以synchronized static 方法可在一个类的范围内被相互间锁定起来,防止与static 数据的接触。

注意如果想保护其他某些资源不被多个线程同时访问,可以强制通过synchronized 方访问那些资源。

2.1     计数器的同步

装备了这个新关键字后,我们能够采取的方案就更灵活了:可以只为TwoCounter 中的方法简单地使用synchronized 关键字。下面这个例子是对前例的改版,其中加入了新的关键字:

2.1.1       代码

import java.awt.*;

import java.awt.event.*;

import java.applet.*;

class TwoCounter2
extends Thread {

    private
boolean
started=
false;

    private TextField
t1= newTextField(5),
t2= newTextField(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
synchronizedvoid
run() {

        while (true) {

            t1.setText(Integer.toString(count1++));

            t2.setText(Integer.toString(count2++));

            try {

                sleep(500);

            } catch (InterruptedException
e) {

            }

        }

    }

    public
synchronizedvoid
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
staticint
accessCount= 0;

    private
static
TextField aCount=
newTextField("0",10);

    public
staticvoid
incrementAccess() {

        accessCount++;

        aCount.setText(Integer.toString(accessCount));

    }

    private Button
start= newButton("Start"),observer=
newButton("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] =
newTwoCounter2(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
staticvoid
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 byonly

// one thread at a time,assuming all

// threads respect syncObject'slock

}

在能进入同步块之前,必须在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.1.2       同步的效率

由于要为同样的数据编写两个方法,所以无论如何都不会给人留下效率很高的印象。看来似乎更好的一种做法是将所有方法都设为自动同步,并完全消除synchronized 关键字(当然,含有synchronized run()的例子显示出这样做是很不通的)。但它也揭示出获取一把锁并非一种“廉价”方案——为一次方法调用付出的代价(进入和退出方法,不执行方法主体)至少要累加到四倍,而且根据我们的具体现方案,这一代价还有可能变得更高。所以假如已知一个方法不会造成冲突,最明智的做法便是撤消其中的synchronized
关键字。

2.2     回顾J a v a B e a n s

现在已理解了同步,接着可换从另一个角度来考察Java Beans。无论什么时候创建了一个Bean,就必须假定它要在一个多线程的环境中运行。这意味着:

(1) 只要可行,Bean 的所有公共方法都应同步。当然,这也带来了“同步”在运行期间的开销。若特别在意这个问题,在关键区域中不会造成问题的方法就可保留为“不同步”,但注意这通常都不是十分容易判断。

有资格的方法倾向于规模很小(如下例的getCircleSize())以及/或者“微小”。也就是说,这个方法调用在如此少的代码片里执行,以至于在执行期间对象不能改变。如果将这种方法设为“不同步”,可能对程序的执行速度不会有明显的影响。可能也将一个Bean 的所有public 方法都设为synchronized,并只有在保证特别必要、而且会造成一个差异的情况下,才将synchronized 关键字删去。

(2) 如果将一个多造型事件送给一系列对那个事件感兴趣的“听众”,必须假在列表中移动的时候可以添加或者删除。

第一点很容易处理,但第二点需要考虑更多的东西。

2.2.1       代码

import java.awt.*;

import java.awt.event.*;

import java.util.*;

import java.io.*;

public
class
BangBean2 extends Canvas
implements Serializable {

    private
int
xm,ym;

    private
int
cSize= 20;
// Circle size

    private String
text= "Bang!";

    private
int
fontSize= 48;

    private Color
tColor= Color.red;

    private
Vector actionListeners = newVector();

    public BangBean2() {

        addMouseListener(new ML());

        addMouseMotionListener(new MM());

    }

    public
synchronizedint
getCircleSize() {

        return
cSize;

    }

    public
synchronizedvoid
setCircleSize(intnewSize){

        cSize =
newSize;

    }

    public
synchronized
String getBangText() {

        return
text;

    }

    public
synchronizedvoid
setBangText(String
newText){

        text =
newText;

    }

    public
synchronizedint
getFontSize() {

        return
fontSize;

    }

    public
synchronizedvoid
setFontSize(intnewSize){

        fontSize =
newSize;

    }

    public
synchronized
Color getTextColor() {

        return
tColor;

    }

    public
synchronizedvoid
setTextColor(Color
newColor){

        tColor =
newColor;

    }

    public
void
paint(Graphics
g){

        g.setColor(Color.black);

        g.drawOval(xm -
cSize / 2, ym-
cSize/ 2, cSize,cSize);

    }

    // This is a multicastlistener, which is

    // moretypically used than the
unicast

    // approachtaken in BangBean.java:

    public
synchronizedvoid
addActionListener(ActionListener
l) {

        actionListeners.addElement(l);

    }

    public
synchronizedvoid
removeActionListener(ActionListener
l) {

        actionListeners.removeElement(l);

    }

    // Notice thisisn't synchronized:

    public
void
notifyListeners() {

        ActionEvent a =
new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED,null);

        Vector
lv = null;

        // Make a copy of the vector in case someone

        // adds a listener while we're

        // calling listeners:

        synchronized (this) {

            lv = (Vector)
actionListeners.clone();

        }

        // Call all the listener methods:

        for (int
i = 0; i <
lv.size(); i++) {

            ActionListener
al = (ActionListener) lv.elementAt(i);

            al.actionPerformed(a);

        }

    }

 

    class ML
extendsMouseAdapter {

        public
void
mousePressed(MouseEvent
e) {

            Graphics
g = getGraphics();

            g.setColor(tColor);

            g.setFont(new Font("TimesRoman", Font.BOLD,
fontSize));

            int
width = g.getFontMetrics().stringWidth(text);

            g.drawString(text, (getSize().width -
width) / 2, getSize().height / 2);

            g.dispose();

            notifyListeners();

        }

    }

 

    class MM
extendsMouseMotionAdapter {

        public
void
mouseMoved(MouseEvent
e) {

            xm =
e.getX();

            ym =
e.getY();

            repaint();

        }

    }

 

    // Testing theBangBean2:

    public
staticvoid
main(String[]
args){

        BangBean2 bb =
new BangBean2();

        bb.addActionListener(new ActionListener() {

            public
void
actionPerformed(ActionEvent
e) {

                System.out.println("ActionEvent"+
e);

            }

        });

        bb.addActionListener(new ActionListener() {

            public
void
actionPerformed(ActionEvent
e) {

                System.out.println("BangBean2 action");

            }

        });

        bb.addActionListener(new ActionListener() {

            public
void
actionPerformed(ActionEvent
e) {

                System.out.println("More action");

            }

        });

        Frame aFrame =
new Frame("BangBean2 Test");

        aFrame.addWindowListener(new WindowAdapter() {

            public
void
windowClosing(WindowEvent
e) {

                System.exit(0);

            }

        });

        aFrame.add(bb, BorderLayout.CENTER);

        aFrame.setSize(300, 300);

        aFrame.setVisible(true);

    }

} /// :~

很容易就可以为方法添加synchronized。但注意在addActionListener()和removeActionListener()中,现在添加了ActionListener,并从一个Vector 中移去,所以能够根据自己愿望使用任意多个。

我们注意到,notifyListeners()方法并未设为“同步”。可从多个线程中发出对这个方法的调用。另外,在对notifyListeners()调用的中途,也可能发出对addActionListener()和removeActionListener()的调用。这显然会造成问题,因为它否定了VectoractionListeners。为缓解这个问题,我们在一个synchronized 从句中“克隆”了Vector,并对克隆进行了否定。这样便可在不影响notifyListeners()的前提下,对Vector
进行操纵。

paint()方法也没有设为“同步”。与单纯地添加自己的方法相比,决定是否对过载的方法进行同步要困难得多。在这个例子中,无论paint()是否“同步”,它似乎都能正常地工作。但必须考虑的问题包括:

(1) 方法会在对象内部修改“关键”变量的状态吗?为判断一个变量是否“关键”,必须知道它是否会被程序中的其他线程读取或设置(就目前的情况看,读取或设置几乎肯定是通过“同步”方法进行的,所以可以只对它们进行检查)。对paint()的情况来说,不会发生任何修改。

(2) 方法要以这些“关键”变量的状态为基础吗?如果一个“同步”方法修改了一个变量,而我们的方法要用到这个变量,那么一般都愿意把自己的方法也设为“同步”。基于这一前提,大家可观察到cSize 由“同步”方法进行了修改,所以paint()应当是“同步”的。但在这里,我们可以问:“假如cSize 在paint()执行期间发生了变化,会发生的最糟糕的事情是什么呢?”如果发现情况不算太坏,而且仅仅是暂时的效果,那么最好保持paint()的“不同步”状态,以避免同步方法调用带来的额外开销。

(3) 要留意的第三条线索是paint()基础类版本是否“同步”,在这里它不是同步的。这并不是一个非常严格的参数,仅仅是一条“线索”。比如在目前的情况下,通过同步方法(好cSize)改变的一个字段已合成到paint()公式里,而且可能已改变了情况。但请注意,synchronized 不能继承——也就是说,假如一个方法在基础类中是“同步”的,那么在衍生类过载版本中,它不会自动进入“同步”状态。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: