加入收藏 | 设为首页 | 会员中心 | 我要投稿 源码门户网 (https://www.92codes.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 运营中心 > 建站资源 > 优化 > 正文

Java传统编程模型存在的问题

发布时间:2019-09-27 07:46:22 所属栏目:优化 来源:格局多
导读:副标题#e# Actor模型不仅仅被认为是一种高效的解决方案 ,它已经在世界上一些要求最苛刻的应用中得到了验证,为了突出Actor模型所解决的问题,本节首先讨论传统编程模型与现代多线程和多CPU的硬件架构之间的不匹配: 对面向对象中封装(encapsulation)特性的
副标题[/!--empirenews.page--]

Actor模型不仅仅被认为是一种高效的解决方案 ,它已经在世界上一些要求最苛刻的应用中得到了验证,为了突出Actor模型所解决的问题,本节首先讨论传统编程模型与现代多线程和多CPU的硬件架构之间的不匹配:

  • 对面向对象中封装(encapsulation)特性的挑战
  • 对共享内存在现代计算机架构上的误解
  • 对调用堆栈的误解

Java传统编程模型存在的问题

对封装特性的挑战

封装(encapsulation)是面向对象编程(OOP)中的一大特性,封装意味着对象内部的数据不能够在对象外直接访问,必须通过对象提供的一系列方法来间接进行访问。对象负责公开对数据的安全操作的方法,以保护其封装的数据的不变性。

在多线程下,多个线程同时调用同一个对象的方法来修改其内部封装的数据时候,就会存在线程安全问题,这是因为封装本身不确保对象内部数据的一致性,在不对对象的方法在修改数据施加一定同步措施时,对象内的数据就会在多线程访问下变得不确定了。

一个解决该问题的方式就是,多线程访问对象方法内数据时候施加一定同步措施,比如加锁,加锁可以保证同时只有一个线程可以访问对象内的数据,但是加锁会带来昂贵的代价:

  • 使用锁会严重影响并发度,使用锁在现在CPU架构中是一个比较昂贵的操作,因为当线程获取锁失败后会把线程从用户态切换到内核态把线程挂起,稍后唤醒后又需要从内核态切换到用户态进行运行。
  • 获取锁失败的调用线程会被阻塞挂起,因此它不能做任何有意义的事情。即使在桌面应用程序中这也是不可取的,我们想要的是即使后台有一个运行比较耗时的工作在运行,也要保证系统对用户的一部分请求有响应。在后端应用中,阻塞是完全浪费资源的。另外可能有人认为虽然当前线程阻塞了,但是我们可以通过启动新线程来弥补这一点,但是需要注意一点,线程也是一种昂贵的资源,操作系统对线程个数是有限制的。
  • 另外锁的存在,带来了新的威胁,即死锁问题的存在。

由于以上问题的存在,导致我们进退两难:

  • 如果不使用足够的锁,则不能保证多线程下对象中数据不受到破坏。
  • 如果在对象中每个数据访问是都加了锁,则会导致系统性能下降,并且很容易导致死锁的发生。

另外,锁只能在单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或者原子性结构进行修饰也是一个困难的事情,这使得我们在非业务逻辑处理上需要耗掉一部分精力。

小结:

  • 在现在多核CPU的硬件架构中,多线程之间不再有真正的共享内存,cpu核心之间显示传递数据块(cache 行)将和网络中不同计算机之间传递数据一样。 CPU核心之间通信和网络通信的共同点比许多人意识到的要多。现在跨CPU或联网计算机传递消息已成为一种规范。
  • 除了通过使用volatile修饰共享的变量或使用原子数据结构保证共享变量内存可见性之外,一个更严格和原则上的方法是将状态保持在并发实体本地,并通过消息显式在并发实体之间传播数据或事件。

对调用堆栈的误解

提起调用堆栈( Call stacks)大家都耳熟能详,但是其被发明是在并发编程不是那么重要时候,那时候多核cpu系统还不常见,所以调用堆栈不会跨线程,因此不会为异步调用链提供调用堆栈能力。

在多线程下,当主线程(调用线程)开启一个异步线程运行异步任务时候,问题就出现了。这时候主线程会将共享对象放到异步线程可以访问到的共享内存里面,然后开启异步线程后主线程继续做自己的事情,而异步线程则会从共享内存中访问到主线程创建的共享对象,然后进行异步处理。

进行异步处理时候遇到的第一个问题是当异步线程执行完毕任务后,如何通知主线程?另外当异步任务执行出现异常时候该怎么做?这个异常将会被异步线程捕获,并且不会传递给主调用线程。

(编辑:源码门户网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!