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 阅读(219) 评论(0)  编辑  收藏

java single threaded_[Java多线程设计模式]读书笔记 - 第一章 Single Threaded Execution相关推荐

  1. 《MAC OS X 技术内幕》读书笔记第一章:MAC OS X的起源

    <MAC OS X 技术内幕>读书笔记第一章:MAC OS X的起源 前言 1 System x.x系列 1.1System 1.0(1984年1月24日) 1.2System 2.x(1 ...

  2. Android群英传神兵利器读书笔记——第一章:程序员小窝——搭建高效的开发环境

    Android群英传神兵利器读书笔记--第一章:程序员小窝--搭建高效的开发环境 目录 1.1 搭建高效的开发环境之操作系统 1.2 搭建开发环境之高效配置 基本环境配置 基本开发工具 1.3 搭建程 ...

  3. mybatis从入门到精通(刘增辉著)-读书笔记第一章

    前言: 本读书笔记共11章节 本版本采用idea编写,不采用作者书中所说的eclipse jdk8 maven3.6.1 mysql5.7 1.idea新建maven项目,配置pom.xml < ...

  4. 深入理解计算机系统第四版_《深入理解计算机系统》读书笔记 —— 第一章 计算机系统漫游...

    本书第一章沿着一个程序的生命周期,简要地介绍一些逐步出现的关键概念.专业术语和组成部分. 一.信息就是位+上下文 在计算机系统中所有的信息都由一串比特来表示. 一串相同的比特(或者几个相同的字节)可以 ...

  5. linux鸟叔私房菜读后感,鸟叔的Linux私房菜 读书笔记 第一章

    目录dom 硬盘数学 第一章 计算机概论 知识点总结 计算机的定义为:接受使用者输入指令与资料,经由中央处理器的数学与逻辑单元运算处理后,以产生或储存成有用的资讯:程序 电脑的五大单元包括:输入单元. ...

  6. 多线程设计模式读书笔记(一)

    1. Single Thread Execution Pattern 1. 概念 该Pattern用来限制多个线程同时对share resource的访问只让一个线程访问,一般通过synchronzi ...

  7. Java编程思想第四版读书笔记——第九章 接口

    这章介绍了适配器设计模式和策略设计模式. 第九章  接口 接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 1.抽象类和抽象方法 public abstract void f(); 创 ...

  8. Linux多线程服务端编程笔记,陈硕Linux多线程服务端编程读书笔记 —— 第一章 线程安全的对象生命周期管理...

    muduo书第一章的思维导图(新窗口打开可以看大图) 线程安全的对象析构和回调(使用shared_ptr和weak_ptr) 注释是英文的(工地英语-) StockFactory.h // in mu ...

  9. 深入理解计算机系统(CSAPP)读书笔记 第一章

    第1章 计算机系统漫游 1.1 信息就是位+上下文 系统中所有的信息--包括磁盘文件.内存中的程序.内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的.区分不同数据对象的唯一方法是我们读到 ...

最新文章

  1. 初学Vue 遇到Module not found:Error:Can`t resolve 'less-loader' 问题
  2. 应用程序文件Android安全分析挑战:运行时篡改Dalvik字节码
  3. java spring框架 注解_详解Java的Spring框架中的注解的用法
  4. python url中传递中文_Python编程:URL网址链接中的中文编码与解码
  5. 【每日SQL打卡】​​​​​​​​​​​​​​​DAY 15丨查询活跃业务【难度中等】
  6. 3.4 多个例子中的向量化
  7. 第二届360杯全国大学生信息安全技术大赛部分解题思路(加密解密题)
  8. 动易cms聚合空间最近访客访问地址错误解决方法
  9. PLC控制系统在我国的发展及应用
  10. dos计算机,dos操作系统,教您进入dos操作系统
  11. 关于VLAN-tag
  12. selenium页面跳转问题-QQ邮箱登录之后找不到元素
  13. V社线下沙龙·深圳站——12.05(周六)
  14. 阿里巴巴与星巴克合作 AR场景识别首次大规模商用
  15. asp数组函数LBound 、UBound和Split
  16. 二叉平衡树 之 红黑树 (手动模拟实现)
  17. 追梦之路-For the dream
  18. 太棒了 | 辞职之后,去了一趟新疆!
  19. bootpdf下载 spring_SpringBoot教程 PDF 下载
  20. 80%的人分不清传感网与物联网的区别,这二者之间的具体区别到底是什么?

热门文章

  1. PHP 阿里云实人认证
  2. Ext JS从零开始之二
  3. NVIDIA视频编码器 ffmpeg -h encoder=h264_nvenc
  4. 小程序源码:朋友圈集赞万能截图生成器微信小程序源码下载
  5. 十六.Elasticsearch Adjacency Matrix Aggregation
  6. mysql biginteger java_java.math.BigInteger cannot be cast to java.lang.Integer以及mysql升级的问题...
  7. 浏览器打不开网页服务器错误代码,浏览器打不开任何网页提示域名解析错误错误代码105怎么办?...
  8. 百度飞桨领航团python零基础训练营笔记
  9. java读取pdf文本转换html
  10. B.FRIEND背光防水静音键盘,高剪刀脚键盘GK4