注:本文的主要参考资料为结城浩所著《JAVA多线程设计模式》。

单线程执行模式(Single Threaded Execution Pattern)是最简单的多线程设计模式,几乎所有其他的模式都在不同程度上应用了该模式。先看一个程序,通过它可以体验多线程程序无法正确执行的场景,这里所写的是个关于“只能单个通过的门”的程序:有三个人频繁地、反复地经过一个只能容许单人经过的门,当人通过门的时候,这个程序显示出通过人的“姓名”与“出生地”,其代码如下:

  1. public class Gate {
  2. private int counter = 0;
  3. private String name = "Nobody";
  4. private String address = "Nowhere";
  5. public void pass(String name, String address) {
  6. this.counter++;
  7. this.name = name;
  8. this.address = address;
  9. check();
  10. }
  11. public String toString() {
  12. return "No." + counter + ": " + name + ", " + address;
  13. }
  14. private void check() {
  15. if (name.charAt(0) != address.charAt(0)) {
  16. System.out.println("***** BROKEN ***** " + toString());
  17. }
  18. }
  19. }
  20. public class UserThread extends Thread {
  21. private final Gate gate;
  22. private final String myname;
  23. private final String myaddress;
  24. public UserThread(Gate gate, String myname, String myaddress) {
  25. this.gate = gate;
  26. this.myname = myname;
  27. this.myaddress = myaddress;
  28. }
  29. public void run() {
  30. System.out.println(myname + " BEGIN");
  31. while (true) {
  32. gate.pass(myname, myaddress);
  33. }
  34. }
  35. }
  36. public class Main {
  37. public static void main(String[] args) {
  38. System.out.println("Testing Gate, hit CTRL+C to exit.");
  39. Gate gate = new Gate();
  40. new UserThread(gate, "Alice", "Alaska").start();
  41. new UserThread(gate, "Bobby", "Brazil").start();
  42. new UserThread(gate, "Chris", "Canada").start();
  43. }
  44. }

这里用到了一个小小的技巧:我们将姓名与出生地的“头一个”字母设计为相同(A、B或者C),因此可以通过校验两者来观察线程间是否有“互窜”的现象。

在PC机上运行一会儿,一定会打印出“***Broken***”字样,说明上述程序存在线程安全问题(确切来说,是Gate.java是非线程安全的类)。

上述现象之所以会发生,关键问题还是出在Gate类pass方法中,详细看一下代码:

  1. public void pass(String name, String address) {
  2. this.counter++;
  3. this.name = name;
  4. this.address = address;
  5. check();
  6. }

为简单说明,现假设只有两个线程(Alice与Bobby),它们每次调用pass的顺序可能是完全随机的,因此会存在某一刻,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"
线程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"

无论发生上述哪一种,都会使name与address出现非预期的结果。以上是没有使用Single Threaded Execution Pattern的情况。如需做线程安全的改造,可将Gate改造为如下:

  1. public class Gate {
  2. private int counter = 0;
  3. private String name = "Nobody";
  4. private String address = "Nowhere";
  5. public synchronized void pass(String name, String address) {
  6. this.counter++;
  7. this.name = name;
  8. this.address = address;
  9. check();
  10. }
  11. public synchronized String toString() {
  12. return "No." + counter + ": " + name + ", " + address;
  13. }
  14. private void check() {
  15. if (name.charAt(0) != address.charAt(0)) {
  16. System.out.println("***** BROKEN ***** " + toString());
  17. }
  18. }
  19. }

在我的机器上,无论多久都没有显示BROKEN消息。这个执行结果虽然不能证明Gate类的安全性,但我们可以说该程序安全的可能性很大。

上述情况之所以会显示BROKEN,是因为pass方法内的程序可能会被多个线程穿插执行。synchronized方法,能够保证同时只有一个线程可以执行它。线程Alice执行pass方法的时候,线程Bobby就不能调用pass方法。在线程Alice执行完pass方法之前,线程Bobby会在pass方法的入口处被阻挡下。当线程Alice执行完pass方法之后,将锁定解除线程Bobby才可以开始执行pass方法。所有,只要将pass方法声明称synchronized的,就绝对不会出现上面表中的情况;而一定是下图的两种情况之一:

线程Alice 线程Bobby this.name的值 this.address的值
【获取锁定】      
this.counter++   (之前的值) (之前的值)
this.name = name   "Alice" (之前的值)
this.address = address   "Alice" "Alaska"
check();   "Alice" "Alaska"
【解除锁定】      
  【获取锁定】    
  this.counter++ "Alice" "Alaska"
  this.name = name "Bobby" "Alaska"
  this.address = address "Bobby" "Brazil"
  check(); "Bobby" "Brazil"
  【解除锁定】    
线程Alice 线程Bobby this.name的值 this.address的值
  【获取锁定】    
  this.counter++ (之前的值) (之前的值)
  this.name = name "Bobby" (之前的值)
  this.address = address "Bobby" "Brazil"
  check(); "Bobby" "Brazil"
  【解除锁定】    
【获取锁定】      
this.counter++   "Bobby" "Brazil"
this.name = name   "Alice" "Brazil"
this.address = address   "Alice" "Alaska"
check();   "Alice" "Alaska"
【解除锁定】      

这里再说明一下,toString方法需要加上synchronized的理由,以及check方法不加上synchronized的理由:

  • 假设线程A正在调用pass方法,而线程B此时正在调用toString,由于线程B在引用name之后再引用address,此间隙线程A可能会改掉address的值,因此可能会输出不一致的name与address;即此时,pass是线程安全的,但toString却不是线程安全的。
  • 由于check方法是private的,这意味着它不会被客户端直接调用,而唯一调用check方法的pass已被设成synchronized了,因此,不需要再将check设置成synchronized方法。虽然将check方法设置成synchronized不会产生问题,但锁定会带来一定的开销,因此完全没有必要。

总的来说,一个多线程下的程序,往往有会一块“限制多个线程访问”的程序块,这部分可称为临界区。临界区的存在一定会使程序的执行性能下降,主要是因为:

  • 获取锁定需要花时间
  • 线程冲突时必须进行等待。当一个线程执行临界区内的操作时,其他要进入临界区的线程会被阻挡。

学习&理解该模式的一个很好的方法,就是每当看见synchronized方法时,都去思考一下“该synchronized是在保护什么东西”?在上面的例子中,这个方法实质上是在保护counter、name以及address字段不会被多个线程同时访问。

如果我们为Gate类添加synchronized的setter方法,它还是线程安全的吗?

  1. public synchronized void setName(String name)
  2. {
  3. this.name = name;
  4. }
  5. public synchronized void setAddress(String address)
  6. {
  7. this.address = address;
  8. }

尽管这些方法都被设置成synchronized了,但是Gate类还是不安全的。因为name与address非得合在一起赋值才行。之所以将pass方法设置成synchronized,主要就是为了不要让多个线程穿插赋值。如果开放出setName、setAddress等方法,线程对字段的赋值操作就被分散了。因此,要保护,就要合在一起保护,否则是没有意义的。

另外,调用synchronized方法的线程,一定会获取this的锁定。一个实例的锁定,某个时刻内只能被一个线程所享用。换句话说,如果实例不同,即使用synchronized方法保护,多个线程还是能各自执行。

转载 http://grunt1223.iteye.com/blog/895046

JAVA并发设计模式学习笔记(二)—— Single Threaded Execution Pattern的更多相关文章

  1. JAVA并发设计模式学习笔记(一)—— JAVA多线程编程

    这个专题主要讨论并发编程的问题,所有的讨论都是基于JAVA语言的(因其独特的内存模型以及原生对多线程的支持能力),不过本文传达的是一种分析的思路,任何有经验的朋友都能很轻松地将其扩展到任何一门语言. ...

  2. java之jvm学习笔记二(类装载器的体系结构)

    java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...

  3. 多线程学习之一独木桥模式Single Threaded Execution Pattern

    Single Threaded Execution Pattern[独木桥模式] 一:single threaded execution pattern的参与者--->SharedResourc ...

  4. 多线程程序设计学习(2)之single threaded execution pattern

    Single Threaded Execution Pattern[独木桥模式] 一:single threaded execution pattern的参与者--->SharedResourc ...

  5. Java设计模式学习笔记(二) 简单工厂模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 正文开始... 1. 简介 简单工厂模式不属于GoF23中设计模式之一,但在软件开发中应用也较为 ...

  6. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  7. 多线程设计模式(一) Single Threaded Execution

    这里有一座独木桥.因为桥身非常的细,一次只能允许一个人通过.当这个人没有下桥,另一个人就不能过桥.如果桥上同时又两个人,桥就会因为无法承重而破碎而掉落河里. 这就是Single Threaded Ex ...

  8. Java并发编程学习笔记 深入理解volatile关键字的作用

    引言:以前只是看过介绍volatile的文章,对其的理解也只是停留在理论的层面上,由于最近在项目当中用到了关于并发方面的技术,所以下定决心深入研究一下java并发方面的知识.网上关于volatile的 ...

  9. Java 并发编程学习笔记 理解CLH队列锁算法

    CLH算法实现 CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁.结点之间是通过隐形的链表相连,之所以叫隐形的链 ...

随机推荐

  1. python 之 Collections模块

    官方文档:https://yiyibooks.cn/xx/python_352/library/collections.html 参考: https://blog.csdn.net/songfreem ...

  2. [Python] WeChat_Robot

    在微信中接入一个聊天机器人 1. WeChat 个人接口itchat 2. 图灵机器人 #-*- coding:utf-8 -*- import itchat import requests apiU ...

  3. 用django实现redirect的几种方法总结

    用django开发web应用, 经常会遇到从一个旧的url转向一个新的url.这种隐射也许有规则,也许没有.但都是为了实现业务的需要.总体说来,有如下几种方法实现 django的 redirect.1 ...

  4. KindEditor 和 xss过滤

    KindEditor   1.进入官网 2.下载 官网下载:http://kindeditor.net/down.php 本地下载:http://files.cnblogs.com/files/wup ...

  5. Thread(线程)三

    今天我们继续接着线程讲讲,上一章提到一下task概念, 首先接着task继续往下讲,在前章节提到过Thread怎么实现其他线程完成后再让主线程继续执行的功能,那么如果Task也需要线程等待事件,该怎么 ...

  6. A Bug's Life(加权并查集)

    Description Background Professor Hopper is researching the sexual behavior of a rare species of bugs ...

  7. BUI 框架使用指南

    指南说明:只适用于对框架的剥离 如果不需要剥离则原来的东西直接粘贴就行 在主界面中使用时需要加入一下引用bui.js jquery.js config.js 末尾的文件 BUI.use(位置1, fu ...

  8. $.getJSON() 回调函数没有执行的原因

    $.getJSON() 方法使用 AJAX 的 HTTP GET 请求获取 JSON 数据. 语法 $.getJSON(url,data,success(data,status,xhr)) url必填 ...

  9. objective-C 的内存管理之-实例分析

    objective-C 的内存管理之-实例分析 注:这是<Objective-C基础教程>一书上的实例,但是原书限于篇幅,分析得比较简单,初次阅读看得比较费劲,这里展开详细讨论一下. 场景 ...

  10. Elasticsearch-PHP 搜索操作

    搜索操作 好吧,这不叫elasticsearch的不劳而获!让我们来谈谈PHP客户端中的搜索操作. 客户端允许你通过REST API访问所有的查询和公开的参数,尽可能的遵循命名规则.让我们来看一些例子 ...