http://blog.sina.com.cn/s/blog_4b6047bc010009co.html
我们知道Java里面有一个类java.lang.reflect.Proxy能实现所谓的动态代理,其核心思想是在运行时动态生成一个类,该类继承了java.lang.reflect.Proxy,实现了指定接口。该类将对于接口方法的调用都动态分派到一个所谓的Handler那儿去,开发者可以实现自己的Handler,截获调用,进行面向方面的处理。因此动态代理这个功能往往用来实现AOP方面的功能。
这个动态类是利用虚拟机生成的。知道动态代理的原理之后,我们完全可以自己实现这样一个动态代理,只要生成该类的class文件的内存映象即可。现在网上由许多这种修改类或者干脆生成类的Java字节码的工具,其中Apache网站的cglib以及它依赖的项目BCEL都可以实现,但是这些类库的缺陷是太过庞大,依赖其他第三方类库的地方太多,造成可实用不是太强,这是现Java开源项目的一个通病。
ObjectWeb上面有个项目叫ASM(含义是Java字节码的汇编语言)。它是一个Java字节码修改框架,能直接生成二进制类代码或者动态修改类代码,生成stub类或者其他类似代理类。ASM要比BCEL和SERP小的多,其核心部分才33KB,速度也要比这些工具快的多,大概要比BCEL快7倍,比SERP快11倍。由于ASM设计的目的就是在运行时使用,因此它的体积尽可能小,速度尽可能快。
动态代理有一个很好的用处就是生成调用stub,截获类调用,添加一些横向功能。这在EJB容器、Web容器等应用服务器实现时尤其有用。采用这种接口,你可以简单的实现面向方面的功能,比如添加安全、事务、日志、过滤、编码、解码等等的功能,而且是纯粹热插拔的模式。总之使用动态代理能够实现许多AOP方面的功能。
并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。
并行一般是指没有互斥和同步的情况下独立进行同时发生的事件。因此单CPU操作系统的进程/线程严格意义上来说都不能算是并行事件,毕竟它们都要使用同一个CPU,真正的并行出现在多处理器的计算机上,当进程/线程独立运行在不同的CPU上,而且没有不需要共享对象时。粗略的说,没有资源互斥共享的进程和线程都是并行的。
如果进程/线程没有共享任何数据,它们编程所关心的许多概念就不会存在。就像现实世界,如果任何两个人都是独立的,没有任何关系的,那么也不会存在社会的各种机构来协调这些关系。在操作系统中,进程之间共享数据的方式一般通过IO(如:文件、管道、网络端口等等),当然也有时会通过内存共享。这种松耦合的共享造成的同步、互斥问题并不多。常见的同步问题发生同一进程内不同线程之间。由于线程存在于同个进程中,它们之间是可以共享内存的,所以就会有很多同步和互斥的问题。
那么什么是同步,什么是互斥?同步和互斥往往是共生的。所谓的同步是指不同实体的动作按照某些特定条件的顺序执行。最常见的莫过于生产者和消费者之间的关系。生产者的生产动作和消费者的消费动作是必须满足先后顺序的:只有生产者生产出东西来,消费者才能消费这些东西。它们之间就需要所谓的同步。什么是互斥?互斥是指两个实体的动作不允许同时发生,如果同时发生就会产生不可以预期的结果。互斥是同步的前提,如果两个动作不是互斥的,就不可能保证其发生的顺序。同步一定是互斥的,而互斥不一定需要同步。同步是固定顺序的动作的互斥。理解这一点非常重要。
举个我们都熟悉的例子,多个并发生产者和多个并发消费者。生产者生产的对象放在一个数组中,而消费者则从这个数组中获取对象。那么生产者的生产动作之间是需要互斥的,但不需要同步。不管是A先放在数组,还是B先放在数组中,它们之间除了为避免将产品放在数组中同一位置上,需要互斥地访问数组外,是不需要规定哪一个在前面放,哪一个在后面放的。它们之间的关系就属于互斥。而生产者和消费者之间就存在一个先后顺序问题,必须至少有生产者生产出产品放在数组中,消费者才能开始消费。因此生产者生产和消费者消费之间的就是同步关系。此外,消费者和消费者之间的消费关系也是需要互斥的,这样才能避免两个消费者之间争夺同数组里同一位置的对象。但是它们的消费行为是不需要同步的,只要互斥的进行就行了。
因此不同实体之间的动作有两个基本关系:同步和互斥。一般处理同步的方法是建立在互斥的基础上的。互斥的机制一般需要通过操作系统甚至底层硬件提供的信号量、管程(管程是建立在信号量基础上的更高层构件)等底层机制来实现。Java语言中通过提供互斥原语synchronized(虽然叫同步,更准确的说应该是互斥mutex)来保证的,当然Java实际上是通过JVM的monitor_enter和monitor_exit指令来实现的,这些指令最终以底层操作系统提供的机制来实现。同步的实现除了要依靠互斥原语,还要结合条件判断和线程挂起等语言构件来实现。其原理比较简单,首先要通过原语synchronized互斥两个需要同步的动作(也称作临界代码),当某个动作(比如消费)获得信号锁进入管程时,它首先判断某个条件是否满足(是否有可消费对象),不满足则挂起当前线程,释放信号锁,允许其他线程进入。当其他线程(比如生产者)进入后,也是检查是否满足某些条件(比如数组是否有空闲),如果不满足则和前面线程一样释放信号锁并挂起线程,如果满足(有空闲)则进行动作(生产并放在数组空闲处),然后这个动作一般要负责激活其他挂起的线程(当然也可以不负责任,其结果是往往造成死锁),然后自己释放信号锁退出管程。其他被激活的线程进入下一轮竞争,谁获得信号锁后继续检查它需要的条件是否满足,如此继续下去。
饥饿
饥饿是指线程长时间无法获得共享资源从而继续相继的处理。这种情况经常发生在当共享资源被“贪婪”线程长时间占据时。假设一个对象提供的互斥方法需要很长时间处理才能返回,然而如果某线程老是频繁激活这个方法,那么其他需要访问该对象的线程就会被长时间阻塞,而处于饥饿状态。
活锁
一种常见的线程动作是响应另外线程的动作。然而如果另外线程的动作恰好也是该线程的响应,那么活锁现象就可能会产生。正如死锁一样,处于活锁状态的线程通常不能继续后续操作,但它们不是处于阻塞状态,而是简单不断地响应彼此的动作。举个例子,如果两朋友A和B在狭窄的走廊里碰面了,A想靠左以便让B通过,B想靠右以便A通过,结果他们仍然互相堵住对方的路,于是A便向右让以便让B通过,B同时也往左让,以便让A通过,于是就他们就如此让来让去,一直下去。这就是活锁。
线程之间共享数据引起了并发执行程序中的同步问题。那些数据是可能需要同步访问的呢?很简单,线程之间能够共享的数据,也就是对多个线程可见的数据。
Java的数据有两种基本类型内存分配模式(不算虚拟机内部类型,详细内容参见虚拟机规范):运行时栈和堆两种。由于运行时栈是线程所私有的,它主要用来保存局部变量和中间运算结果,因此它们的数据是不可能被线程之间所共享的。内存堆是创建类对象和数组地方,它们是被虚拟机内各个线程所共享的,因此如果一个线程能获得某个堆对象的引用,那么就称这个对象是对该线程可见的。
线程之间通信基本上通过共享对象引用来达到共享对象的简单类型字段和引用字段。由于不涉及I/O操作,这种模式的共享比IPC共享要高效的多。但也使得两类错误成为可能:线程干扰和内存一致性错误。防止此类问题发生的线程编程技术称作同步。我们详述一下这两个错误的概念。
线程干扰
考察下面的一段代码:
class Counter {
private intc = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter的目的是对increment方法的调用将c增加1,对decrement的调用将c减去1。然而如果一个Counter对象被多个线程所引用,那么这些线程之间的干扰就让我们经常得不到期望的结果。
当运行在不同线程中对同一对象进行访问的两个操作发生时,干扰就会产生。这是因为这两个操作往往是由多步组成的,而且它们的执行顺序是可以互相交织的。
表面看来,Counter对象的increment和decrement操作是不可能交织的,每个操作都是一个简单的Java语句。实际上这些语句都已经被虚拟机翻译成了好几步的指令。我们不再详细描述虚拟机所采用的指令步骤,只需知道一个c++操作可能被虚拟机分为三步:
1.获取c的当前值
2.将该值加上1
3.将加的结果保存回变量c
c--也是同样三步,除了第二步进行减操作外。
假设A线程调用increment方法的同时B线程调用decrement方法,而c的初始值为0,那么它们的交织的指令动作可能依着下面的顺序进行:
1. 线程A: 获取c.
2. 线程B: 获取c.
3. 线程A:获取的值加1,结果1
4. 线程B:获取的值减1,结果-1
5. 线程A:将1保存到c变量中,c结果是1
6. 线程B:将-1保存到c变量中,c结果是-1
线程A的结果丢失了,被线程B的覆盖了。这种特殊的交织顺序只产生一种结果。但在另一种情况下,也可能B的结果被覆盖,或者干脆没有错误。由于它们执行顺序的不确定性,线程干扰的错误将很难定位和修改。
内存一致性错误
当不同线程对同一个数据看到不同视图时,内存一致性错误就发生了。内存一致性错误发生的原因很复杂,不是一两句话能解释得清楚的,在这儿就不再详述。我们只需知道防备这种错误发生的策略就行了。
避免内存一致性错误的关键是理解“发生过”(happens-before)关系,这个关系是保证被某语句写过内存的结果对其他某个语句是可见的。为理解这一点,考虑下面的例子,假设有个简单的int字段这样定义:
intcounter=0;
这个counter字段在两个线程A和B之间共享。假设线程A增加counter:
counter++;
紧接着,线程B打印出counter:
System.out.println(counter);
如果这两句话在同一个线程内执行,那么假设打印结果为1是安全的。但如果当这两个语句是在不同线程中执行的,那么打印出的值完全有可能是0。因为线程A对于counter的改变不一定能对线程B可见,除非程序在这两条语句之间建立了“发生过”关系。
有几种动作能建立“发生过”关系,目前我们已经接触的能产生“发生过”关系的动作包括:
*当语句调用Thread.start方法,任何和Thread.start建立有“发生过”关系的语句和新启动线程执行的每条语句都有“发生过”关系。创建新线程代码的效果对于这个新线程是可见的。
*当线程结束并导致另一个线程的Thread.join返回,那么结束线程所执行的所有语句和join后面的语句都有“发生过”关系。