java对象可变状态风险

来源:互联网 发布:灰鸽子源码 vc版 编辑:程序博客网 时间:2024/06/05 02:46

*****摘自《七周七并发编程》《深入理解java虚拟机》


多线程下变量修改的可见性

现代CPU一般都使用读写速度很快的高速缓存来作为内存和cpu之间的缓冲,高速缓存的引入可以有效解决CPU和内存之间的速度矛盾。但同时引入了新的问题:缓存一致性。多CPU系统,每个处理器都有自己的高速缓存cache,而高速缓存共享相同内存,为了解决缓存一致性问题,需要各个处理器访问缓存时遵循一定的协议。
此外,为了获取好的执行效率,处理器可能会对代码进行乱序执行优化,处理器会再计算之后将乱序执行的结果进行重组,保证该结果与顺序执行的结果一致。但并不保证程序中各个语句执行的顺序与代码的顺序一致。
java虚拟机中,即时编译器也会进行指令重排序优化,是在机器层面的优化,执行的线程是无法感知这种优化的。
java内存模型规定了所有的变量都存储在主内存中, 除此之外每个线程都有自己的工作内存, 线程的工作内存中保存了被该线程使用到的变量的副本拷贝, 线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行, 而不能直接读写主内存中的变量. 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成.
由上可知, 一个线程修改了变量的值, 另一个线程并非总是能够及时获知最新的值, 这就是可见性问题的根源。

隐藏的可变状态

class DataParser{private final DataFormat format = new SimpleDateFormat("yyyy-MM-dd");public Date parse(String s) throws ParseException{return format.parse(s)}}
多个线程使用这个类的同一对象时,首次运行可能得到如下错误:java.lang.NumberFormatException:for input string :".12012E4.12012E4"

再次运行,可能会得到如下错误:caught:java.lang.ArrayIndexOutOfBoundsException:-12012E4

第三次运行,可能还会得到另一个错误:java.lang.NumberFormatException:multiple points

虽然这段代码只有一个成员变量,且被标记为不可变(即final),但显然这段代码根本达不到线程安全,为什么呢?造成的原因是SimpleDateFormat内部有隐藏的可变状态,你可能会认为这应该是个bug,但对我们来说是不是bug并不重要,Java这类语言为了让代码写起来简单,在此隐藏了可变状态,也是我们无法判断何时会发生错误——从API无法判断SimpleDateFormat是否是线程安全的。隐藏的可变状态还不是唯一需要留意的问题,我们再来看一个。

逃逸的可变状态

假设我们要创建一个管理比赛的web服务,需求是能管理一个运动员列表,我们会习惯的写如下代码:

public class Tournament{private List<Player> players = new LinkedList<Player>();public synchronized void addPlayer(Player p ){players.add(p);}public synchronized Iterator<Player> getPlayerIterator(){return players.iterator();}}
通常我们会认为这段代码是线程安全的——players是私有变量,仅addPlayer()和getplayerIterator()使用,且两个方法都标记了synchronized,然而它并不是线程安全的,因为getPlayerIterator()返回的迭代器仍引用了players内部的可变状态——如果迭代器在使用时,另一个线程调用了addPlayer()方法,那么程序就会抛出ConcurrentModificationException或变得更糟,也就是说可变状态从Tournament在重重防护下逃逸了。

在并发程序中,隐藏和逃逸仅仅是两种可变状态带来的风险——还有件很多其他的风险。

this逃逸

指构造函数返回之前其它线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引起错误。this逃逸经常发生在构造函数中启动线程或注册监听器时, 如:

public class ThisEscape {      public ThisEscape() {          new Thread(new EscapeRunnable()).start();          // ...      }      private class EscapeRunnable implements Runnable {          @Override          public void run() {              // 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸          }      }  }  



逃逸分析

逃逸分析,是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其它优化技术提供依据的分析技术。逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传到其它方法中,称为方法逃逸。甚至还有可能被其它线程访问到,比如赋值给类变量或可以在其它线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。(1)栈上分配;(2)同步消除;(3)标量替换。




1 0
原创粉丝点击