Eros Live
Find the Way
posts - 15,comments - 0,trackbacks - 0
Single Threaded Execution是指“以1个线程执行”的意思。就像细独木桥只允许一个人通行一样,这个Pattern用来限制同时只让一个线程运行。

Single Threaded Execution将会是多线程程序设计的基础。务必要学好。

Single Threaded Execution有时候也被称为Critical Section(临界区)。

Single Threaded Execution是把视点放在运行的线程(过桥的人)上所取的名字,而Critical Section则是把视点放在执行的范围(桥身)上所取的名字。

范例程序1:不使用Single Threaded Execution Pattern的范例

首先,我们先来看一个应该要使用Single Threaded Execution Pattern而没有使用和程序范例。这个程序的用意是要实际体验多线程无法正确执行的程序,会发生什么现象。

模拟3个人频繁地经过一个只能容许一个人经过的门。当人通过门的时候,这个程序会在计数器中递增通过的人数。另外,还会记录通过的人的“姓名与出生地”

表1-1 类一览表
--------------------------------------------------------------
 名称                         说明
--------------------------------------------------------------
Main            创建一个门,并操作3个人不断地穿越门的类
Gate            表示门的类,当人经过时会记录下姓名与出身地
UserThread       表示人的类,只负责处理不断地在门间穿梭通过
--------------------------------------------------------------

Main类

Main类(List 1-1)用来创建一个门(Gate),并让3个人(UserThread)不断通过。创建Gate对象的实例,并将这个实例丢到UserThread类的构造器作为参数,告诉人这个对象“请通过这个门”。

有下面3个人会通过这个门:

Alice - Alaska
Bobby - Brazil
Chris - Canada

为了便于对应两者之间的关系,笔者在此故意将姓名与出生地设成相同的开头字母。

在上线程中,先创建3个UserThread类的实例,并以start方法启动这些线程。

List 1-1 Main.java
--------------------------------------
public class Main {
    public static void main(String[] args) {
        System.out.println("Testing Gate, hit CTRL+C to exit.");
        Gate gate = new Gate();
        new UserThread(gate, "Alice", "Alaska").start();
        new UserThread(gate, "Bobby", "Brazil").start();
        new UserThread(gate, "Chris", "Canada").start();
    }
}
--------------------------------------

并非线程安全的(thread-safe)的Gate类

Gate类(List 1-2)表示人所要通过的门。

counter字段表示目前已经通过这道门的“人数”。name字段表示通过门的行人的“姓名”,而address字段则表示通过者的“出生地”

pass是穿越这道门时使用的方法。在这个方法中,会将表示通过人数的counter字段的值递增1,并将参数中传入行人的姓名与出生地,分别拷贝到name字段与address字段中。

this.name = name;

toString方法,会以字符串的形式返回现在门的状态。使用现在的counter、name、address各字段的值,创建字符串。

check方法,用来检查现在门的状态(最后通过的行人的记录数据)是否正确。当人的姓名(name)与出生地(address)第一个字符不相同时,就断定记录是有问题的。当发现记录有问题时,就显示出下面的字符串:

****** BROKEN ******
并接着调用toString方法显示出现在门的状态。

这个Gate类,在单线程的时候可以正常运行,但是在多线程下就无法正常执行。List 1-2 的Gate类是缺乏安全性的类,并不是线程安全(thread-safe)的类。

List 1-1 非线程安全的Gate类(Gate.java)

    public class Gate {
        private int counter = 0;
        private String name = "Nobody";
        private String address = "Nowhere";
        public void pass(String name, String address) {
            this.counter++;
            this.name = name;
            this.address = address;
            check();
        }
        public String toString() {
            return "No." + counter + ": " + name + ", " + address;
        }
        private void check() {
            if (name.charAt(0) != address.charAt(0)) {
                System.out.println("***** BROKEN ***** " + toString());
            }
        }
    }

UserThread类

UserThread类(List 1-3)表示不断穿越门的行人。这个类被声明成Thread类的子类。

List 1-3 UserThread.java

    public class UserThread extends Thread {
        private final Gate gate;
        private final String myname;
        private final String myaddress;
        public UserThread(Gate gate, String myname, String myaddress) {
            this.gate = gate;
            this.myname = myname;
            this.myaddress = myaddress;
        }
        public void run() {
            System.out.println(myname + " BEGIN");
            while (true) {
                gate.pass(myname, myaddress);
            }
        }
    }

为什么会出错呢?

这是因为Gate类的pass方法会被多个线程调用的关系。pass方法是下面4行语句程序代码所组成:

this.counter++;
this.name = name;
this.address = address;
check();

为了在解说的时候简单一点,现在只考虑两个线程(Alice和Bobby)。两个线程调用pass方法时,上面4行语句可能会是交错依次执行。如果交错的情况是图1-3这样,那调用check方法的时候,name的值会是“Alice”,而address的值会是“Brazil”。这时就会显示出 BROKEN了。

图1-3 线程Alice与线程Bobby调用pass方法时的执行状况
-----------------------------------------------------------------------------------
线程Alice               线程Bobby               this.name的值        this.address的值
-----------------------------------------------------------------------------------
this.counter++         this.counter++
                               this.name = name        "Bobby"  
this.name = name                                       "Alice"
this.address = address                                "Alice"             "Alaska"
                              this.address = address  "Alice"             "Brazil"
check()                  check()                         "Alice"             "Brazil"
****** BROKEN ******
-----------------------------------------------------------------------------------

或者说交错的情况如图1-4所示,则调用check方法的时刻,name的值是"Bobby",而address的值会是"Alaska"。这个时候也会显示出BROKEN。

图1-4 线程Alice与线程Bobby调用pass方法的执行状况
------------------------------------------------------------------------------------
线程Alice              线程Bobby                this.name的值        this.address的值
------------------------------------------------------------------------------------
this.counter++        this.counter++ 
this.name = name                                        "Alice"
                             this.name = name           "Bobby"
                             this.address = address    "Bobby"              "Brazil"
this.address = address                                 "Bobby"              "Alaska"
check()                 check()                           "Bobby"              "Alaska"
****** BROKEN ******
------------------------------------------------------------------------------------

上述哪一种情况,都使字段name与address的值出现非预期的结果。
通常,线程不会去考虑其他的线程,而自己只会一直不停地跑下去。“线程Alice现在执行到的位置正指定name结束,还没有指定address的值”,而线程Bobby对此情况并不知情。

范例程序1之所以会显示出BROKEN,是因为线程并没有考虑到其他线程,而将共享实例的字段改写了。
对于name字段来说,有两个线程在比赛,赢的一方先将值改写。对address来说,也有两个线程在比赛谁先将值改写。像这样子引发竞争(race)的状况,我们称为race condition。有race condition的情况时,就很难预测各字段的值了。


以上是没有使用Single Threaded Execution Pattern时所发生的现象。

范例程序2:使用Single Threaded Execution Pattern的范例

线程安全的Gate类
List 1-4 是线程安全的Gate类。需要修改的有两个地方,在pass方法与toString方法前面都加上synchronized。这样Gate类就成为线程安全的类了。

List 1-4 线程安全的Gate类(Gate.java)

    public class Gate {
        private int counter = 0;
        private String name = "Nobody";
        private String address = "Nowhere";
        public synchronized void pass(String name, String address) {
            this.counter++;
            this.name = name;
            this.address = address;
            check();
        }
        public synchronized String toString() {
            return "No." + counter + ": " + name + ", " + address;
        }
        private void check() {
            if (name.charAt(0) != address.charAt(0)) {
                System.out.println("***** BROKEN ***** " + toString());
            }
        }
    }

synchronized所扮演的角色

如前面一节所说,非线程安全的Gate类之所以会显示BROKEN, 是因为pass方法内的程序代码可以被多个线程穿插执行。
synchronized 方法,能够保证同时只有一个线程可以执行它。这句话的意思是说:线程Alice执行pass方法的时候,线程Bobby就不能调用pass方法。在线程 Alice执行完pass方法之前,线程Bobby会在pass方法的入口处被阻挡下。当线程Alice执行完pass方法之后,将锁定解除,线程 Bobby才可以开始执行pass方法。

Single Threaded Execution Pattern的所有参与者

SharedResource(共享资源)参与者

Single Threaded Execution Pattern中,有担任SharedResource角色的类出现。在范例程序2中,Gate类就是这个SharedResource参与者。

SharedResource参与者是可以由多个线程访问的类。SharedResource会拥有两类方法:

SafeMethod   - 从多个线程同时调用也不会发生问题的方法
UnsafeMethod - 从多个线程同时调用会出问题,而需要加以防护的方法。

在Single Threaded Execution Pattern中,我们将UnsafeMethod加以防卫,限制同时只能有一个线程可以调用它。在Java语言中,只要将UnsafeMethod定义成synchronized方法,就可以实现这个目标。

这个必须只让单线程执行的程序范围,被称为临界区(critical section)

                                                 :SharedResource
---------                                   -----------------
:Thread  -----------------------|-> synchronized|
---------                                  |  UnsafeMethod1|
                                               |                           |
---------                                  |                           |
:Thread  ---------------------->|   synchronized|
---------                                   |  UnsafeMethod2|
                                                 -----------------

扩展思考方向的提示

何时使用(适用性)

多线程时

单线程程序,并不需要使用Single Threaded Execution Pattern。因此,也不需要使用到synchronized方法。

数据可以被多个线程访问的时候

会需要使用Single Threaded Execution Pattern的情况,是在SharedResource的实例可能同时被多个线程访问的时候。
就算是多线程程序,如果所有线程完全独立运行,那也没有使用Single Threaded Execution Pattern的必要。我们将这个状态称为线程互不干涉(interfere)。
有些管理多线程的环境,会帮我们确保线程的独立性,这种情况下这个环境的用户就不必考虑需不需要使用Single Thread Execution Pattern。

状态可能变化的时候

当SharedResource参与者状态可能变化的时候,才会有使用Single Threaded Execution Pattern的需要。
如果实例创建之后,从此不会改变状态,也没有用用Single Threaded Execution Pattern的必要。

第二章所要介绍的Immutable Pattern就是这种情况。在Immutable Pattern中,实例的状态不会改变,所以是不需要用到synchronized方法的一种Pattern。

需要确保安全性的时候

只有需要确保安全性的时候,才会需要使用Single Threaded Execution Pattern。
例如,Java的集合架构类多半并非线程安全。这是为了在不考虑安全性的时候获得更好的性能。
所以用户需要考虑自己要用的类需不需要考虑线程安全再使用。

生命性与死锁

使用Single Threaded Execution Pattern时,可能会发生死锁(deadlock)的危险。
所谓死锁,是指两个线程分别获取了锁定,互相等待另一个线程解除锁定的现象。发生死锁的时,两个线程都无法继续执行下去,所以程序会失去生命性。

举个例子:

假设Alice与Bobby同吃一个大盘子所盛放的意大利面。盘子的旁边只有一支汤匙和一支叉子,而要吃意大利面时,需要同时用到汤匙与叉子。

只有一支的汤匙,被Alice拿去了,而只有一支的叉子,去被Bobby拿走了。就造成以下的情况:

握着汤匙的Alice,一直等着Bobby把叉子放下。
握着叉子的Bobby,一直等着Alice的汤匙放下。

这么一来Alice和Bobby只有面面相觑,就这样不动了。像这样,多个线程僵持不下,使程序无法继续运行的状态,就称为死锁。

Single Threaded Execution达到下面这些条件时,可能会出现死锁的现象。

1.具有多个SharedResource参与者
2.线程锁定一个SharedResource时,还没有解除锁定就前去锁定另一个SharedResource。
3.线程获取SharedResource参与者的顺序不固定(和SharedResource参与者对等的)。

回过头来看前面吃不到意大利面的两个人这个例子。
1.多个SharedResource参与者,相当于汤匙和叉子。
2.锁定某个SharedResource的参与者后,就去锁定其他SharedResource。就相当于握着汤匙而想要获取对方的叉子,或握着叉子而想要获取对方的汤匙这些操作。
3.SharedResource角色是对等的,就像“拿汤匙->拿叉子”与“拿叉子->拿汤匙”两个操作都可能发生。也就是说在这里汤匙与叉子并没有优先级。

1, 2, 3中只要破坏一个条件,就可以避免死锁的发生。具体的程序代码如问题1-6

 

问题1-7

某人正思考着若不使用synchronized,有没有其他的方法可以做到Single Threaded Execution Pattern。而他写下了如下的Gate类,如代码1。那么接下来就是问题。请创建Gate类中所要使用的Mutex类。

顺带一提,像Mutex类这种用来进行共享互斥的机制,一般称为mutex。mutex是mutual exclusion(互斥)的简称。

代码1

    public class Gate {
        private int counter = 0;
        private String name = "Nobody";
        private String address = "Nowhere";
        private final Mutex mutex = new Mutex();
        public void pass(String name, String address) { // 并非synchronized
            mutex.lock();
            try {
                this.counter++;
                this.name = name;
                this.address = address;
                check();
            } finally {
                mutex.unlock();
            }
        }
        public String toString() { //  并非synchronized
            String s = null;
            mutex.lock();
            try {
                s = "No." + counter + ": " + name + ", " + address;
            } finally {
                mutex.unlock();
            }
            return s;
        }
        private void check() {
            if (name.charAt(0) != address.charAt(0)) {
                System.out.println("***** BROKEN ***** " + toString());
            }
        }
    }


解答范例1:单纯的Mutex类

下面是最简单的 Mutex类,如代码2。在此使用busy这个boolean类型的字段。busy若是true,就表示执行了lock;如果busy是false,则表示执行了unlock方法。lock与unlock双方都已是synchronized方法保护着busy字段。

代码2

    public final class Mutex {
        private boolean busy = false;
        public synchronized void lock() {
            while (busy) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
            busy = true;
        }
        public synchronized void unlock() {
            busy = false;
            notifyAll();
        }
    }

代码2所示的Mutex类在问题1-7会正确地执行。但是,若使用于其他用途,则会发生如下问题。这就是对使用Mutex类的限制。这意味着Mutex类的重复使用性上会有问题。

问题点1
假设有某个线程连续两次调用lock方法。调用后,在第二次调用时,由于busy字段已经变成true,因此为wait。这就好像自己把自己锁在外面,进不了门的意思一样。

问题点2
即使是尚未调用出lock方法的线程,也会变成可以调用unlock方法。就好比即使不是自己上的锁,自己还是可以将门打开一样。

解答范例2:改良后的Mutex类

代码3是将类似范例1中的问题予以改良而形成的新的Mutex类。在此,现在的lock次数记录在locks字段中。这个lock数是从在lock方法调用的次数扣掉在 unlock方法调用的次数得出的结果。连调用lock方法的线程也记录在owner字段上。我们现在用locks和owner来解决上述的问题点。

代码3

    public final class Mutex {
        private long locks = 0;
        private Thread owner = null;
        public synchronized void lock() {
            Thread me = Thread.currentThread();
            while (locks > 0 && owner != me) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
            //assert locks == 0 || owner == me
            owner = me;
            locks++;
        }
        public synchronized void unlock() {
            Thread me = Thread.currentThread();
            if (locks == 0 || owner != me) {
                return;
            }
            //assert locks > 0 && owner == me
            locks--;
            if (locks == 0) {
                owner = null;
                notifyAll();
            }
        }
    }


测试代码

代码4

    public class UserThread extends Thread {
        private final Gate gate;
        private final String myname;
        private final String myaddress;
        public UserThread(Gate gate, String myname, String myaddress) {
            this.gate = gate;
            this.myname = myname;
            this.myaddress = myaddress;
        }
        public void run() {
            System.out.println(myname + " BEGIN");
            while (true) {
                gate.pass(myname, myaddress);
            }
        }
    }

代码5

    public class Main {
        public static void main(String[] args) {
            System.out.println("Testing Gate, hit CTRL+C to exit.");
            Gate gate = new Gate();
            new UserThread(gate, "Alice", "Alaska").start();
            new UserThread(gate, "Bobby", "Brazil").start();
            new UserThread(gate, "Chris", "Canada").start();
        }
    }
posted on 2008-03-08 14:54 Eros 阅读(255) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航: