Java多线程设计模式(2)
《Java多线程设计模式》读书笔记2
目录:
1 Java的内存模型
2 Single Threaded Execution Pattern
3 Guarded Suspension Pattern
4 Balking Pattern
5 Producer-Consumer Pattern
6 Read-Write Lock Pattern
=============Java的内存模型======================
Java的内存模型分为主存储器与工作存储器两种。
主存储器(与硬件上的主存无关,仅为抽象概念)就是实例位置所在的区域,所有的实例都存在于主存储器内。尤其是实例所拥有的字段即位于主存储器内的区域。主存储器为所有线程所共有。工作存储器为各个线程所拥有的作业区,所有的线程都有其专用的工作存储器。在工作存储器中存有主存储器中必要的拷贝,称之为工作拷贝。
线程无法直接对主存储器直接进行操作,因此也无法直接引用字段的值,当线程需要引用字段的值时,需要把它从主存储器上拷贝到自己的工作存储器中。至于要修改字段的值时,也要先修改工作拷贝,然后把修改从工作存储器拷贝回主存储器中。至于何时完成数据在工作存储器和主存储器之间的映像,则有Java执行处理系统决定。
对于多个线程共享的字段,一般由synchronized或者volatile来保护。
synchronized用来完成线程的共享互斥。而volatile不负责线程的共享互斥,仅负责内存的同步。
synchronized一方面制作临界区,使得该区域只能同时允许一个线程进行操作;另一方面,当进入和退出临界区时,执行内存的同步操作,即把当前工作区的修改映像到主存储器上。在进入synchronized时,除了工作拷贝到主存储器的映像之外,还会释放工作存储器的工作拷贝,如果之后再引用主存储器上值,则必须从主存储器拷贝到工作存储器。
虽然在进入和离开synchronized时,会发生内存的同步,但是如果是“在synchronized内”或者“在synchronized外”时,内存何时同步是不确定的,取决于Java运行处理系统。
在单线程中,可以不考虑内存的同步问题,当然线程的同步也不需考虑。
volatile内存同步:当线程欲引用volatile字段的值时,通常都会发生从主存储器到工作存储器的拷贝操作,反之,将值指定给写着volatile的字段后,工作存储器的内容通常会映像到主存储器。另外,对于long和double类型,默认的赋值操作不是atomic的,如要以atomic方式指定这些类型的值,则需使用volatile关键字,此时,volatile实现原子操作,而非内存同步。问:如果要指定long,double类型字段的内存同步,怎么办?难道只能用synchronized来实现?
=============Single Threaded Execution Pattern=============
当我们修改由多个线程共享的变量,变量就会失去安全性。所以我们应该仔细找出变量状态不安定的范围,将这个范围设置为临界区,并对临界区施加保护,限制同时只能有一个线程执行它。在java中,可以使用synchronized关键字定义临界区,保护共享字段。
适用性:数据可被多个线程访问,并且状态可能变化的时候。
===================Immutable Pattern===============
当一个类的实例声明之后,状态就完全不再改变,此时就算用多个线程同时调用这个类的方法也无所谓,所以方法没有synchronized的必要,这样一来,可以在不丧失安全性与生命性的前提下,提高程序的性能。而要实现类的immutable,则在类的实现中必须小心防备,不要提供能够改变实例状态的方法或者破绽。
Java final的用法
final类:final类无法被继承
final方法:如果是实例方法,则无法被子类所覆盖,若是类方法,则无法被子类隐藏。
final字段:其值只能指定一次,要么在声明时初始化,要么在构造函数中赋值(静态字段则是在static块中初始化)。
final变量与参数:final变量只能在定义时指定一次,final参数则是仅在调用时传值指定。
===================Guarded Suspension Pattern===============
当满足某条件时,直接执行某动作;如果不满足条件,等待,直到条件满足时被唤醒。
比如请求队列,如果队列已经为空,则此时要执行获取请求操作则必须等待;当添加新请求时唤醒等待的线程。
public synchronized Request getRequest(){
while(queue.size() <= 0){
try{
wait();
}catch(InterruptedException e){}
return (Request)queue.removeFirst();
}
public synchronized void putRequest(Request req){
queue.addLast(req);
notifyAll();
}
===================Balking Pattern===============
当现在不适合进行某个操作,或者没有必要进行某个操作时,就直接放弃进行这个操作而返回。
public synchronized void save(){
if(!changed){
return;
}
dosave();
changed = false;
}
使用场景:(1)不需要刻意去执行的时候(提高执行性能);(2)不想等待警戒条件时(提高响应性);(3)警戒条件只有第一次成立时(比如初始化)。
===================Producer-Consumer Pattern===============
Producer-Consumer模式假设所生产的产品放置在一个长度有限制的缓冲区,如果缓冲区满了,则生产者必须停止继续将产品放到缓冲区中,直到消费者取走了产品而有了空间,而如果缓冲区中没有产品,当然消费者必须等待,直到有新的产品放到缓冲区中。与Guarded Suspension 模式类似的,只不过Guarded Suspension模式并不限制缓冲区的长度。
可能有多个生产者和多个消费者,当只有一个生产者和一个消费者的时候,又叫管道Pipe Pattern。
Java主要通过synchronized,wait与notify/notifyAll方法来实现producer与consumer的协调。
===================Read-Write Lock Pattern===============
Read-Write Lock Pattern将读取与写入分开处理。在读取数据之前,必须获取用来读取的锁定。而要写入的时候,则必须获取用来写入的锁定。
因为读取的时候,实例的状态不会改变,所以就算有多个线程在同时读取也没有关系。但有人在读取的时候就不可做写入的操作。
写入的时候,实例的状态就会改变,当有一个线程在写入的时候,其他的线程不可以进行读取或写入。一般来说,进行共享互斥会使程序性能变差,但将写入的共享互斥与读取的共享互斥分开思考,就可以提升程序的性能。
Java语言中,使用finally可以避免忘记解除锁定。比如:
获取锁定
try{
实际的操作
}finally{
解除锁定
}