Java传统编程模型存在的问题
Actor模型不仅仅被认为是一种高效的解决方案 ,它已经在世界上一些要求最苛刻的应用中得到了验证,为了突出Actor模型所解决的问题,本节首先讨论传统编程模型与现代多线程和多CPU的硬件架构之间的不匹配:
对封装特性的挑战 封装(encapsulation)是面向对象编程(OOP)中的一大特性,封装意味着对象内部的数据不能够在对象外直接访问,必须通过对象提供的一系列方法来间接进行访问。对象负责公开对数据的安全操作的方法,以保护其封装的数据的不变性。 在多线程下,多个线程同时调用同一个对象的方法来修改其内部封装的数据时候,就会存在线程安全问题,这是因为封装本身不确保对象内部数据的一致性,在不对对象的方法在修改数据施加一定同步措施时,对象内的数据就会在多线程访问下变得不确定了。 一个解决该问题的方式就是,多线程访问对象方法内数据时候施加一定同步措施,比如加锁,加锁可以保证同时只有一个线程可以访问对象内的数据,但是加锁会带来昂贵的代价:
由于以上问题的存在,导致我们进退两难:
另外,锁只能在单JVM内(本地锁)很好的工作。当涉及到跨多台机协调时,只能使用分布式锁。但是分布式锁的效率比本地锁低几个数量级,这是因为分布式锁协议需要跨多台机在网络上进行多次往返通信,所以其造成较大的影响就是延迟。 小结:
对共享内存在现代计算机架构上的误解 在80-90年代的编程模型概念化地表示,写入变量时候是直接把其值写入主内存里面(这有点混淆了局部变量可能只存在于cpu寄存器中的事实)。在现在计算机硬件架构中,计算机系统中为了解决主内存与CPU运行速度的差距,在CPU与主内存之间添加了一级或者多级高速缓冲存储器(Cache),每个cache有好多cache行组成,这些Cache一般是集成到CPU内部的,所以也叫 CPU Cache。所以当我们写入变量的时候实际是写入到了当前cpu的Cache中,而不是直接写入到主内存中,并且当前cpu核对自己cache写入的变量对其他cpu核是不可见的,这即是Java内存模型中共享变量的内存不可见问题。 在JVM中我们可以把变量使用volatile关键字修饰或者使用JUC包中的原子性变量比如AtomicLong对普通变量进行包装来保证多线程下共享变量的内存可见性,当然使用加锁的方式也可以保证内存可见性,但是其开销更大。既然使用volatile关键字可以解决共享变量内存可见性问题,那么为何不把所有变量都使用volatile修饰那?这是因为使用volatile修饰变量,写入该变量的时候会把cache直接刷新会内存,读取时候会把cache内缓存失效,然后从主内存加载数据,这就破坏了cache的命中率,对性能是有损的。 所以我们需要细心的分析哪些变量需要使用volatile修饰,但是即使开发人员意识到volatile可以解决内存不可见问题,但是从系统中找出哪些变量需要使用volatile或者原子性结构进行修饰也是一个困难的事情,这使得我们在非业务逻辑处理上需要耗掉一部分精力。 小结:
对调用堆栈的误解 提起调用堆栈( Call stacks)大家都耳熟能详,但是其被发明是在并发编程不是那么重要时候,那时候多核cpu系统还不常见,所以调用堆栈不会跨线程,因此不会为异步调用链提供调用堆栈能力。 在多线程下,当主线程(调用线程)开启一个异步线程运行异步任务时候,问题就出现了。这时候主线程会将共享对象放到异步线程可以访问到的共享内存里面,然后开启异步线程后主线程继续做自己的事情,而异步线程则会从共享内存中访问到主线程创建的共享对象,然后进行异步处理。 进行异步处理时候遇到的第一个问题是当异步线程执行完毕任务后,如何通知主线程?另外当异步任务执行出现异常时候该怎么做?这个异常将会被异步线程捕获,并且不会传递给主调用线程。 理论上主调用线程需要在异步任务执行完毕或者出异常时候被通知,但是没有调用堆栈可以传递异常。异步任务执行失败的的通知只能通过辅助方式完成,比如Future方式,将错误码写到主调用线程所在的地方,否则一旦准备好就希望得到结果。如果没有此通知,则主调用线程将永远不会收到失败通知,并且任务将丢失!这类似于网络系统的工作方式,其中消息/请求可能会丢失/失败而不会发出任何通知。 (编辑:ASP站长网) |