Java多线程设计模式

来源:互联网 发布:js startwith 用法 编辑:程序博客网 时间:2024/04/28 11:54
 Introduction 1  Java语言的线程

开始执行Java程序后,至少会有一个线程开始操作,有操作的是被称为主线程的线程,主线程执行输入类的main()。当main()里的所有处理均结束后,则主线程也同时结束。

当应用程序的规模大到一定程度,程序里的多线程会以每种形式存在。以下是几个常见的范例:
1)  GUI应用程序。
2)  比较花费时间的I/O处理。
3)  多个客户端。

如欲启动线程时,有下列两种方法:
1)  利用Thread类的子类的实例,启动线程。
2)  利用Runnable接口的实现类的实例,启动线程。

要注意的是,“Thread的实例”和“线程本身”是两个不同的部分。即使建立了Thread的实例,也还没有启动线程,而且就算线程已经结束,Thread实例也不会就这样消失。

建立一个实现Runnable接口的类,将该类的实例传给Thread的构造函数,调用start()...,这就是利用Runnable接口来启动线程的方法。

记住:无论哪种方式,启动新线程的方法永远是Thread类的start()。

利用Thread类的sleep()可以暂停线程的执行。执行下面的语句时,即可让当前的线程(执行此语句的线程)暂时停止越1000ms。
    Thread.sleep(1000);

当实例方法加上关键字synchronized声明之后,就可以只让一个线程操作某类实例的这类方法。“让一个线程操作”并不是说只能让某一个特定的线程操作而已,而是指一次只能让一个线程执行。这种方法称为synchronized方法(同步方法)。

synchronized实例方法是使用this锁定去做线程的共享互斥。synchronized类方法是使用该类的类对象的锁定去做线程的共享互斥。

线程的协调(这三种方法执行的前提是执行线程手中有obj的锁):
1)  obj.wait()是把现在的线程放到obj的wait set;
2)  obj.notify()是从obj的wait set里唤醒一个线程;
3)  obj.notifyAll()是唤醒所有在obj的wait set里的线程。
被唤醒的线程只是处于等锁状态(但已经不再wait set中了),当时的obj的锁还掌握在执行notify()或notifyAll()的线程手上。
当唤醒的线程得到锁之后,将从wait()之后继续执行。

Introduction 2  多线程程序的评量标准

安全性--不损坏对象。
生存性--进行必要的处理。(liveness)
复用性--可再利用类。
性  能--能快速、大量进行处理。

第1章  Single Threaded Execution - 能通过这座桥的,只有一个人

Single Threaded Execution是指“以一个线程执行”的意思。就象细独木桥只能允许一个人通过一样,这个模式用来限制只让一个线程运行。

在Single Threaded Execution Pattern中,我们将unsafeMethod加以防卫,限制同时只能有一个线程可以调用它(加上synchronized)。这个必须让单线程执行的程序范围,我们称为“临界区”(critical section)。

使用Single Threaded Execution Pattern时,可能会有发生死锁(deadlock)的危险。

当SharedResource的字段开放给子类访问时,可能会因为子类写出unsafeMethod而导致丧失安全性。

synchronized方法和synchronized块,无论碰到return或是异常,都会确实解除锁定。

结论,Java语言规范中:
1)  基本类型、引用类型的指定、引用是原子的操作。
2)  但是long和double的指定、引用是可以分割的。
3)  要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile。

第2章 Immutable - 想破坏它也没办法

类被声明为final的,属性都是final和private的。属性只能通过构造方法来设置,没有修改属性值的方法。

Thread.currentThread().getName()是用来得到自己这个线程(所对应的java.lang.Thread类实例)的名称。

当字符串和类实例表达式以+运算符连接的时候,会自动调用实例表达式的toString()方法(这是Java的规定)。

类图中,类的字段名称中加上{frozen}的限制,这是UML中表现“建立实例并初始化字段后就不能再次修改其值”的方式。
类图中,类的方法名称中加上{concurrent}的限制,这是UML中表现“多个线程同时执行也无妨”的方式(指Java中不需要加上synchronized的方法)。

当类声明为final中,这个类无法被继承。

当实例方法被声明为final中,这个方法无法被子类所覆盖(override)。
当类方法被声明为final中,这个方法无法被子类所隐藏(hide)。

final的字段的值只能指定一次:
1)  对于final的实例字段,要么在声明字段时就直接赋初值,要么在构造方法中将值赋给字段。
2)  对于final的类字段,要么在声明字段时就直接赋初值,要么在static块(静态初始化子)中将值赋给字段。

final的局部变量的值(在函数内)只能够指定一次。
final参数的值(在函数内)则一次都不能指定,因为当方法被调用时,已经有值指定进去了。

第3章  Guarded Suspension - 要等到我准备好喔

对象图或交互图中,如果类的边框用粗线,则代表该对象和线程有相关性,意即该对象可以主动地调用方法(这是UML规定的表示法)。被称为主动对象。

第4章  Balking - 不需要的话,就算了吧

Thread类(以及其子类)的实例,一旦调用start()后,就会变成“结束start”状态。
如果start()被再次调用时,就会进行balk让线程的启动不会再次执行,并且抛出IllegalThreadStateException。
也就是说,Thread的start()就是本章所说的“不能执行两次以上”,即start()使用了Balking模式。

第5章  Producer-Consumer - 我来做,你来用

上面所说的思维若整理成口诀,可以得到这样两句:
1)  线程的合作要想“放在中间的东西”。
2)  线程的互斥要想“应该保护的东西”。

如果是多个Producer和一个Consumer的话,只有Comsumer线程会访问的范围,就不需要考虑共享互斥了,这样可以提升程序的性能。

习惯编写Java多线程代码后,就会习惯去注意方法的后面有没有throw InterruptedException。如果方法有这个,通常告诉我们下面两件事:
1)  这是“需要花点时间”的方法。
2)  这是“可以取消”的方法。

Java的标准库中,后面接着throw InterruptedException的方法的有这三个:
1)  java.lang.Object的wait()。
2)  java.lang.Thread的sleep()。
3)  java.lang.Thread的join()。

当sleep中的线程被调用interrupt()时,就会放弃暂停的状态,(sleep线程)并抛出InterruptedException异常。
当对wait中的线程调用interrupt()时,(wait线程)会先重新获得锁定,在抛出InterruptedException异常。
当join(等待其他线程结束)的线程被调用interrupt()时,就会放弃等待状态,(join线程)并抛出InterruptedException异常。

notify()和interrupt()对wait中的线程调用时意义有点相近,但是仍有差异:
1)  notify/notifyAll是java.lang.Object的方法,是该实例的wait set调用的。而不是对线程直接调用。notify/notifyAll所唤醒的线程,会前进到wait()的下一条语句。另外,执行notify/notifyAll方法需要获得类的实例。
2)  interrupt是java.lang.Thread的方法,是对该线程直接调用的,当被interrupt的线程正在sleep、wait或join时,会抛出InterruptException异常。执行interrupt(取消其他线程),不需要获取该线程的锁定。

interrupt()方法只会改变被interrupt的线程的中断状态(interrupt status)而已。
线程在执行sleep、wait、join时,是在这些方法内不断检查interrupt status的值,如果true了,则自己抛出InterruptedException。
如果在线程还没有执行sleep、wait、join前,就去interrupt它,并不会影响它的正常工作,只有它执行到sleep、wait、join时,才马上抛出InterruptedException。

Thread类的实例方法isInterrupt()可以用来检查指定线程的interrupt status。
Thread类的  类方法interrupted()会检查当前线程的interrupt status并清楚之。

第6章  Read-Wirte Lock - 大家想看就看吧,不过看的时候不能写喔

Java语言中,使用finally可以避免忘记解锁。

Berfore/After Patern的结构:
  before();
  try {
    execute();
  } finally {
    after();
  }
在此,before()在try之外,表示“如果在before()的执行过程中发生异常,就不执行execute()和after()”。

第7章 Thread-Per-Message - 这个工作交给你了

一般而言,我们可以说一个进程里面可以建立多条的线程。

进程和线程的最大的差异在于内存能否共享。
因为线程间的内存是共享的,所以线程之间的沟通可以使用很自然、简单的方式做到。而因为同一个实例可由多个线程同时访问,所以需要正确地进行共享互斥。

切换执行中的线程时,线程和进程一样,需要进行context-switch的操作。然而,线程所管理的context信息比进程要来得少,一般而言线程的context-switch操作要比进程快得多。

第8章  Worker Thread - 等到工作来,来了就工作

invocation与execution分离的用处:
1)  提高响应性:如果invocation与execution无法分离,当execution很花时间时,invocation的操作将被牵连。如果将invocation与execution事先分离,即使execution花时间,invocation也可以继续自己前进。这样能提高程序的响应性。
2)  控制实行顺序:如果invocation与execution无法分离,一旦invoke出来,就必须直接把它execute完。但,如果invocation与execution分离,execute的顺序就可以于invoke的次序无关。也就是说,我们可以对Request设立优先级,控制Channel传递Request给Worker的顺序。
3)  可取消和可重复执行:若能分离invocation与execution,就有办法做到“虽然invocation了,但将execution取消”的功能。同样,如果把Request保存下来,就可以做到重复execute。
4)  分散处理的第一步:因为invocation与execution分离了,所以invoke与execute的操作也容易拆开在两台计算机上执行。相当于Request的对象,可通过网络传送到另一台计算机。

Runnable对象,可以作为方法的自变量传递、堆到队列里、通过网络传递、甚至存进文件中。而这样的一个Runnable对象可以经过多次传来传去,最后传到某台计算机的某条线程上,才真正交付执行。
这时,Runnable接口就可以看作是GoF的Command Pattern中的Command。

当Swing组建一旦被实现,可能改变组件状态的程序代码、依赖于状态的程序代码,都必须交给Event-dispatching thread执行。

第9章  Future - 先给您这张提货单

第10章  Two-Phase Termination - 块把玩具收拾好,去睡觉吧

等待指定的线程结束时,要使用join()方法。另外,检查指定的线程现在是否结束了,可以使用java.lang.Thread的isAlive()方法。若返回值是true,该线程还活着,反之表示线程已经结束了。

java.lang.Runtime的实例方法addShutdownHook()会在Java执行环境全部结束时(调用System.exit()方法或所有非Daemon线程都结束时),调用指定Thread的start()方法(这时的Thread称为shutdown hook)。使用这个方法,可以编写整个程序的终止处理。

调用interrupt()方法后,可以中断掉线程。这里所说的中断掉线程,是指下面其中一种结果:
1)  线程变成“中断状态”对“状态”的反应。
2)  抛出“异常InterruptException”对“控制的反应”。
通常会是1)。只有线程在sleep、wait、join时会是2)(这个时候不会变成“中断状态”)。
然而,1)和2)是可以互相转换的。

中断状态-->InterruptedException异常的转换:
  if  (Thread.interrupted()) {
    throw new InterruptedException();
  }
  在花时间的处理前,先加上这个if语句,可提高程序对中断的响应性,可以避免不知道自己已经被中断,还开始进行花时间的操作。
  调用Thread.interrupted()方法后,当前线程就不是中断状态了,也就是说,只要调用一次Thread.interrpupted()方法后,中断状态就会被清除。
  如果不想清除中断状态,而要检查当前线程是否被中断,要使用inInterrupted()实例方法,使用方式如下:
  if  (Thread.currentThread().inInterrupted()) {
    // 若为中断状态时需要进行的处理(中断状态不会清除)
  }

InterruptedException异常-->转换为中断状态:
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
  }
  这样可以将收到的InterruptedException,转换为中断状态的形式。
  
InterruptedException异常-->转换为InterruptedException异常:
  收到的InterruptedException异常,也可以不马上抛出去,而留下来晚点再抛:
  InterruptedException savedException = null;
  ...
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    savedException = e;
  }
  ...
  if (savedException != null) {
    throw savedException;
  }

第11章  Thread-Specific Storage - 每个线程的保管箱

Thread-Specific Storage Pattern是只有一个入口,但内部会对每个线程提供特有存储空间的Pattern。
如果使用Java标准链接库时,可以使用java.lang.ThreadLocal类加以实现。

java.lang.ThreadLocal的实例可以想像成一种集合结构或许会比较好理解。
ThreadLocal的set()方法,可以将参数所指定的实例,存放到调用set()的当前线程所对应的存储空间。
ThreadLocal的get()方法,可以将调用get()的当前线程所对应的存储空间中的对应的实例,取出返回(如果没有set过,则返回null)。

手边有一个假定单线程作为执行环境的对象。现在我们想将这个对象放在多线程环境下执行,又不想修改使用端的线程,也不能改变对象的接口。这时就使用Thread-Specfic Storage Pattern。
在此将目的对象当作TSObject,并建立与TSObject具有相同接口的TSObjectProxy,另外,为了管理“Client-->TSObject”的对照表,又加上TSObjectCollection。TSObjectProxy会使用TSObjectCollection取得当前线程所对应的TSObject,而将工作委托给这个TSObject。

第12章  Active Objects - 接受异步消息的主动对象

在多线程程序设计中,千万要随时意识到每个方法是由哪个线程调用的。

与平常不同的是,在这里的方法是由Client以外的线程在执行的。也就是说,Active Object Pattern做到了“异步的方法”。也可以说是做到了“异步的消息”。

再看看ActiveObject包的属性,可以看到,通过ActiveObject包中的所有的参与者的相互协调,组合成一个“主动对象”,这个主动对象,具有:
1)  具有接口(API)-ActiveObject interface定义了API。
2)  可以获取异步消息-Proxy参与者会将方法调用转换成MethodRequest对象,存放在ActivationQueue里。
3)  与Client调用不同的线程-Scheduler提供线程。
4)  可以执行处理-Servant可以单线程执行处理。
5)  可以返回返回值-以Future作为返回值的提货单。
请回想本章前面的比喻,许多人互相协调,可以构成一个“法人”。Active Object Pattern则是协调许多对象,构成一个“主动对象”。

Active Object Pattern是否适合使用要考虑问题的处理量的大小。处理量太小不适合使用的原因,是因为Proxy建立ConcreteMethodRequest与ActivationQueue进行沟通,这些工作的需要花费的时间也不容忽视。

我们可以把Active Object Pattern看作是在以单线程为前提的Servant外面包上一层皮,使多线程的Client可以使用它。

总结 - 多线程程序设计的模式语言

Single Threaded Execution Pattern - 能通过这座桥的,只有一个人
别名:Critical Section,Critical Region
背景:多个线程共享一个实例。
问题:若多个线程都擅自更改实例的状态,实例会丧失安全性。
解决方式:
首先,仔细找出实例状态不稳定的范围(临界区域)。并对临界区域加以防护,使同时执行的线程保持在只有一条的情况。这样一来,就能够保护住实例的安全性。
实现:Java语言里,可使用synchronized来实现出临界区间。
相关:
当实例的状态不会改变时,为了提升性能,可使用Immutable Pattern。
想要将引用实例状态的线程和改变实例状态的线程拆开,以提高性能使,可使用Read-Write Lock Pattern。

Immutable Pattern - 想破坏它也没办法
背景:多个线程共享一个实例,实例的状态不会改变。
问题:使用Single Threaded Execution Pattern,会降低性能。
解决方式:
当实例建立后状态就不会变化时,就要停止使用Single Threaded Execution Pattern。
为了避免失误造成更改了实例的状态,故将类写成无法由线程更改。另外,删除实例里所有用来更新状态的方法(setter)。引用实例状态用的方法(getter)就无妨。
使用Immutable Pattern可提高性能,但是要持续保证不变性(Immutability)并不简单,记得在帮助文件中也要注明这是一个immutable的类。
实现:在Java中使用private来隐藏字段,此外,由于无法确保不可更改,因此还要使用final。
相关:
当多个线程进行共享互斥,可使用Single Threaded Execution Pattern。
当修改用的线程数量比用来读取的线程数量多时,可考虑使用Read-Write Lock Pattern。

Guarded Suspension Pattern - 要等到我准备好喔
别名:Spin Lock,Guarded Wait
背景:多个线程共享一个实例。
问题:若多个线程都擅自更改实例的状态,实例会丧失安全性。
解决方式:
当实例的状态不恰当时,就要求线程等待到适合的状态。首先,以“警戒条件”来表示实例的“适当的状态”。并且在进行有安全性疑惑的操作前,都要检查是否警戒条件满足。如果警戒条件不成立,就要求线程等待到成立为止。
使用Guarded Suspension Pattern,能以警戒条件限制方法的执行。不过,如果警戒条件一直不成立,线程会永远等待下去,会使程序丧失生命性。
实现:Java语言中,检验警戒条件时使用while语句,而要让线程等待时则使用wait()。并使用notify()/notifyAll()通知警戒条件的改变。检验、修改警戒条件时,会使用到Single Threaded Execution Pattern。
相关:
当警戒条件不成立时想要马上退出,就使用Balking Pattern。
Guarded Suspension Pattern中检验、更改警戒条件的部分,会用到Single Threaded Execution Pattern。

Balking Pattern - 不需要的话,就算了吧
背景:多个线程共享一个实例。
问题:若多个线程都擅自更改实例的状态,实例会丧失安全性。可以一直等待安全的时机,又会使程序响应性降低。
解决方式:
当实例的状态不适合时,就中断掉处理的进行。首先,以“警戒条件”来表示实例的“适当的状态”,并且在进行有安全性疑惑的操作前,都要检查是否满足警戒条件。只有在警戒条件成立时,才会继续执行;如果警戒条件不成立,就直接中断(Balk)执行,马上退出。
实现:Java语言中,检验警戒条件时要使用if 语句。当要balk时,可使用return退出方法。或使用throw抛出异常。检验、更改警戒条件时,会使用到Single Threaded Execution Pattern。
相关:
当想要等到警戒条件成立再执行时,可使用Guarded Suspension Pattern。
Balking Pattern中检验、更改警戒条件的部分,会使用到Single Threaded Execution Pattern。

Producer-Consumer Pattern - 我来做,你来用
背景:当要从某个线程(Producer)将数据传给其他线程(Consumer)时。
问题:当Producer与Consumer处理的速度不同时,速度慢的会扯速度快的后腿,而降低程序的性能。另外,当Producer要写入数据时,Consumer若同时读数据,数据会失去安全性。
解决方式:
在Producer与Consumer之间,加上中继用的Channel,并让Channel存放多条数据。这样一来,就可以缓冲Producer和Consumer之间处理速度的差异。另外,只要在在Channel里进行共享互斥,数据就不会丧失安全性。于是性能可以不降低,又可在多个线程间安全地传送数据。
相关:
Channel安全传递数据的部分,使用了Guarded Suspension Pattern。
Future Pattern在传递返回值的时候,使用了Producer-Consumer Pattern。
Worker Pattern在传递请求时,使用了Producer-Consumer Pattern。

Read-Wirte Lock Pattern- 大家想看就看吧,不过看的时候不能写喔
别名:Reader/Writer Lock,Readers/Writers Lock
背景:多条线程共享一个实例,并会有参考实例状态的线程(Reader)与会改变实例状态的线程(Writer)。
问题:若线程之间不进行共享互斥,会丧失安全性。但使用Single Threaded Execution Pattern会是程序西工内能降低。
解决方式:
首先,将“控制Reader的锁定”与“控制Writer的锁定”分开,加入ReaderWriteLock,以提供两种不同的锁定。ReadWriteLock会对“Writer-Writer”、“Reader-Writer”进行互斥控制。但对“Reader-Reader”不进行共享互斥。这样可以在不影响安全性的前提小提高性能。
实现:Java语言可以使用finally块避免忘记解除锁定。
相关:
Read-Write Lock Pattern中,ReadWriteLock进行共享互斥的地方,使用了Guarded Suspension Pattern。
完全没有Writer的时候,可以使用Immutable Pattern。

Thread-Per-Message Pattern - 这个工作交给你了
背景:线程(Client)要调用实例(Host)的方哪国发。
问题:在方法的属性处理完之前,控制权不会从Host退出,如果方法的处理很花时间,程序的响应性会降低。
解决方式:
在Host里,启动新的线程,并且将方法应该进行的工作,交给这个新线程。这样Client的线程就可以继续执行下一个操作了。这样做,不用更改Client的程序代码,并能提高程序的响应性。
实现:Java语言中,为了简化启动线程的程序,可使用匿名的内部类。
相关:
想节省启动线程所花费的时间,可以使用Worker Thread Pattern。
想要将处理的结果返回给Client时,可以使用Future Pattern。

Worker Thread Pattern- 等到工作来,来了就工作
别名:Thread Pool,Background Thread
背景:线程(Client)要调用实例(Host)的方法。
问题:如果问题的处理很花时间,程序的响应会降低。为了提高响应性,而启动新的线程来处理方时,启动线程所花时间又会降低性能。另外,当送出的请求态度时,会启动过多的线程,这会使承受量变差。
解决方式:
首先,我们事先启动一些用来进行处理的线程(Worker Thread)。并将代表请求的实例传给Worker线程,这样就不需要每次都重新启动新的线程了。
相关:
想要获取Worker线程的处理结果时,可以使用Future Pattern。
想要将代表的实例传递给Wroker线程时,可以使用Producer-Consumer Pattern。

Future Pattern- 先给您这张提货单
背景:线程(Client)会将工作委托给其他线程,而Client希望得到处理的结果。
问题:将工作委托给别人时,如果又等待执行结果,会使响应性降低。
解决方式:
首先,建立一个与处理结果具有相同接口的Future,在处理开始时,先把Future当作返回值返回。处理的结果事后再设置给Future。这样Client就可以在适当的时机,通过Future获得(等待)处理的结果。
相关:
等待Client的处理结果时,会使用Guarded Suspension Pattern。
Future Pattern可用在Thread-Per-Message想要获取处理结果时。
Future Pattern可用在Worker Thread Pattern想要获取处理结果时。

Two-Phase Termination Pattern- 快把玩具收拾好,去睡觉吧
背景:想要结束运行中的线程。
问题:从外部忽然结束掉线程,会丧失安全性。
解决方式:
首先,适合进行终止的时机,还是要交给线程自己判断。所以,定义一个送出“终止请求”的方法用来结束线程。这个方法事实上只会将标识设置成“收到终止请求”而已,线程要在每个可以开始终止处理的地方检查这个标识,如果检查结果为真,就开始进行终止处理。
实现:Java语言中,不但要设置表示收到终止请求的标识,还要使用interrupt()中断掉wait、sleep、join的等待状态。因为线程到wait、sleep、join抛出InterruptedException以后,就不是中断状态了,所以若是使用isInterrupted()来检查终止请求,必须特别小心。
相关:
进行终止处理时,为了禁止其他操作,可使用Balking Pattern。
为了确实进行终止处理,使用了Before/After Pattern。

Thread-Specific Storage Pattern - 每个线程的保管箱
别名:Per-Thread Attribute,Thread-Specific Data、Thread-Specific Field、Thread-Local Storage
背景:想要将假定在单线程环境下运行的对象(TSObject),在多线程的环境下使用。
问题:想要使用TSObject并不简单。要将TSObject改写成支持多线程,可能一不小心就丢掉安全性和生命性了。而且,TSObject可能根本不能改写,而我们也不想改写使用TSObject的对象(Client)的程序代码,所以也不想修改TSObject的接口。
解决方式:
建立线程独有的空间,并管理这些空间与线程的对照关系。
首先,建立一个与TSObject具有相同接口的TSObjectProxy参与者。并建立TSObjectCollention,管理“Client-->TSObject”的对照关系。
TSObjectProxy会通过TSObjectCollention获取当前线程所对应的TSObject,并将工作委托给TSObject。Client会拿TSObjectProxy来代替TSObject使用。
这样一来,每个TSObject一定只会有特定的一个线程调用他,所以TSObject不需要进行共享互斥。关于多线程的部分,都隐藏在TSObjectCollention里了。另外,TSObject的接口也不必修改。
不过,使用Thread-Specific Pattern等于是在程序里加上隐性的context,有程序的可读性可能变差的危险性。
实现:Java语言中,使用java.lang.ThreadLocal类担任TSObjectCollention。
相关:
要对多数的线程进行共享互斥时,要使用Single Threaded Execution Pattern。

Active Objects Pattern- 接受异步消息的主动对象
别名:Actor,Concurrent Object
背景:这里有送出请求的线程(Client)与时机用来进行处理的对象(Servant)。可是Servant是假定在单线程环境下运行开发出来的。
问题:想要以多个Client使用Srevant,而Servant又不是线程安全。如果Servant处理的操作很花时间,还会拖垮Client的响应性。
解决方式:
我们在此建立一个可接受异步消息的主动对象,让他拥有独立于Client的线程。
首先,在这里加入一个Scheduler。Servant是有Scheduler调用的。这是只有一个Worker线程的Worker Thread Pattern。这样一来,Servant就不需要支持多线程,也能处理多个Client的请求。
Client送出请求的操作实现时是调用Proxy。Proxy会将请求转换成一个对象,并使用Producer-Consumer Pattern,传给Scheduler。这样,就算Servant进行处理需要花时间,也不会拖慢Client的响应性。
接下来,选杂要执行的请求,则是Scheduler的工作。执行请求的顺序是由Scheduler决定的。
执行结果可使用Future Pattern返回给Client。
相关:
事先Scheduler的部分,使用了Worker Thread Pattern。
从Proxy将请求传给Scheduler的部分,使用了Producer-Consumer Pattern。
对Client返回执行结果时,使用了Future Pattern。

附录B  Java 的内存模型

Java的内存模型分为主存储器(main memory)和工作存储器(working memory)两种。主存储器是对象实例位置所在的区域,所有的实例都存在于主存储器内。主存储器为所有的线程所共用。工作存储器为各线程独立拥有的工作区,在工作存储器中存在有主存储器必要部分的拷贝,称之为工作拷贝(working copy)。

线程无法对主存储器直接进行操作,因此它无法直接引用/指定字段的值。当线程预引用字段的值时,会一次将值从主存储器拷贝到该线程的工作存储器,线程就可以引用这个工作拷贝。但再度引用同一字段的值时,线程是使用前一次的工作拷贝,还是重新从主存储器同步新的工作拷贝,则是由Java执行处理系统决定的。类似的,当线程预将值指定给字段时,会一次将值指定给位于工作存储器上的工作拷贝,但工作拷贝什么时候会映像到主存储器,则是由Java执行处理系统决定的,当线程反复指定同一字段,有可能每次都同步到主存储器,也可能只有最后一次的工作拷贝被同步到主存储器,这还是由Java执行处理系统决定的。

主存储器好比是大家都看得到的黑板;而工作存储器则象是每个学生的笔记本。

synchronized的两项功能:
1)  当线程要进入synchronized时:
a)  如果工作存储器有未映像到主存储器的工作拷贝,则该内容就会被强制写入主存储器。这样之前的计算结果会被全部写入主存储器,因而可以被其他线程看到。
b)  紧接着,工作存储器上的工作拷贝将会被全部丢弃。之后,欲引用主存储器上的值,必定会从主存储器将值拷贝到工作拷贝。
2)  当线程要退出synchronized时:
如果如果工作存储器有未映像到主存储器的工作拷贝,则该内容就会被强制写入主存储器。这样之前的计算结果会被全部写入主存储器,因而可以被其他线程看到。

也就是说,在内存同步方面,当线程欲进出synchronized时,便会将自己工作存储器的内容完全映像到主存储器。

volatile的两项功能:
1)  进行内存的同步。当对volatile的字段进行读操作前,该字段会主存储器拷贝到工作存储器,当volatile的字段进行写操作后,该字段会工作存储器拷贝到主存储器。
2)  以atomic的方式来进行long、double的指定。
atomic和内存同步是两回事,所以,被数个线程进行变更/引用的字段,即使是int型,也必须以synchronized或volatile来保护。

Double Checked Locking Pattern的危险性
为了提高性能,可能会写出下面这样的Double Checked Locking Pattern的代码:
class MySystem {
  private static MySystem instance = null;
  …
  public static MySystem getInstance() {
    if (instance == null) {
      synchronized (MySystem.class) {
        if (instance == null) {
          instance = new MySystem();  // (d)
        }
      }
    }
    return instance;
  }
}
但这样的代码是有安全性的问题的,原因在于,在刚执行完(d)的工作且还未退出synchronized时,初始化完成信息还存在于当前线程的工作存储器上,不保证映像到主存储器。而这是如果其他线程获得控制权,会因为instance不为null(这里意思是instance已经被映像到主存储器,而MySystem的其他实例字段可能还为映像到主存储器),而直接访问主存储器中的instance实例的字段信息,但这些信息在主存储器中还是无效的。

正确的解决方法是要么牺牲性能:
class MySystem {
  private static MySystem instance = null;
  …
  public static synchronized MySystem getInstance() {
    if (instance == null) {
      instance = new MySystem();  // (d)
    }
    return instance;
  }
}
要么更加简化:
class MySystem {
  private static MySystem instance = new MySystem();
  …
  public static synchronized MySystem getInstance() {
    return instance;
  }
}
原创粉丝点击