zoukankan      html  css  js  c++  java
  • JAVA并发设计模式学习笔记(二)—— Single Threaded Execution Pattern

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

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

    Java代码  收藏代码
    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.   
    21. public class UserThread extends Thread {  
    22.     private final Gate gate;  
    23.     private final String myname;  
    24.     private final String myaddress;  
    25.     public UserThread(Gate gate, String myname, String myaddress) {  
    26.         this.gate = gate;  
    27.         this.myname = myname;  
    28.         this.myaddress = myaddress;  
    29.     }  
    30.     public void run() {  
    31.         System.out.println(myname + " BEGIN");  
    32.         while (true) {  
    33.             gate.pass(myname, myaddress);  
    34.         }  
    35.     }  
    36. }  
    37.   
    38. public class Main {  
    39.     public static void main(String[] args) {  
    40.         System.out.println("Testing Gate, hit CTRL+C to exit.");  
    41.         Gate gate = new Gate();  
    42.         new UserThread(gate, "Alice""Alaska").start();  
    43.         new UserThread(gate, "Bobby""Brazil").start();  
    44.         new UserThread(gate, "Chris""Canada").start();  
    45.     }  
    46. }  



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

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

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

    Java代码  收藏代码
    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改造为如下: 

    Java代码  收藏代码
    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方法,它还是线程安全的吗? 

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



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

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

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

  • 相关阅读:
    shell脚本的常用参数
    Qt中使用Protobuf简单案例(Windows + msvc)
    使用PicGo和Typora写Markdown
    CentOS7安装protobuf(C++)和简单使用
    protobuf编译、安装和简单使用C++ (Windows+VS平台)
    protocol buffers 文档(一)-语法指导
    Base64编码和其在图片的传输的应用
    Qt程序打包发布
    Qt中的Label和PushButton背景图自动缩放设置
    TCP的粘包和拆包问题及解决
  • 原文地址:https://www.cnblogs.com/chenying99/p/3321878.html
Copyright © 2011-2022 走看看