java 对象的数据共享与安全

2016-07-27 20:23:38   最后更新: 2016-07-27 20:23:38   访问数量:277




很久以前,公司的老一代程序员(现在都已经离职)在古老的 restlet 框架的基础上创造了一个 Java MVC 框架,一段框架在程序员的手中代代相传,一个传说流传在程序员之间“所有的 controller 都必须打上 scope=prototype 的烙印”,当这个项目流转到我的手上,当时尚不知道 prototype 意味着什么的我在这个项目上种下了一个小小的 bean,打开了上线系统这个魔盒,等待着项目中的这个种子生根发芽,项目早已是饱经线下测试,我自然是胸有成竹,然而,说时迟那时快,客服电话已是纷至沓来,魔盒中的种子释放出的是混乱与灾难,虽是急急回滚,依然造成了一个多小时的线上故障 -- 所有的用户打开我的订单列表,看到的都是别人的订单

记一次重大事故 -- 非线程安全框架引发的意外数据共享

 

如上面的博文中所述,这个真实的案例发生在 2015 年秋季,日志中也介绍了什么是 prototype,虽然项目饱经测试,但是问题依然没能避免,这就是一场线程安全所引发的灾难,也足见并发环境中的问题难以测试和复现

那么,难道真的所有的 Controller 都必须加上 scope=prototype 来多例运行吗?如果是这样,那么为什么 Controller 不能默认打上这个标签吗?

事实上,这是老一代程序员为我们留下的巨坑,框架中所有的 Controller 都是一颗巨大的继承树的叶子,顺藤摸瓜,这个继承树上的各个节点上都出于“方便”的目的,定义了很多类成员变量,类成员变量在同一个类的所有线程之间是共享的,也因此出现了用户共享别人的订单列表的情况

本节我们就来看看,java 中哪些数据是在对象之间共享的呢?如果在开发中不注意这个问题,那么并发环境下的灾难就不可避免了

 

上面提到的事故,奇妙的看到其他用户订单列表的现象,就是因为多线程环境下类可见性的问题,各线程操作公共可见的共享数据时没有必要的同步机制,事实上,这些共享数据本就是不应共享的

 

public class IntegerValue { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }

 

上面这个例子看上去非常简单,没有什么问题,事实上他是非线程安全的,因为当我们每次 get 时,都有可能取到的不是最新的数据,这就造成了失效数据的问题

失效数据可能造成非常严重的安全问题,比如使用某个已经为空值的引用、无限循环等,比如在订单系统中,当你执行 get 方法时,返回的 value 大于 0,此时你可能因此认为货品仍有余量,可以正常购买,结果事实上,同时又有另一个线程执行了 set 操作将 value 置为 0,这之后,你购买的商品就形成了超卖

 

解决失效数据问题可以通过加锁的方式:

public class IntegerValue { private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }

 

这样,在执行 get 的同时,其他企图执行 set 操作的线程将阻塞

 

jvm 标准中,允许将 64 位的变量分解为两个 32 位操作,因此在读写 long 和 double 变量时,可能并不是一个院子操作,从而很可能造成线程安全问题

可以将 long 和 double 变量设置为 volatile 类型从而避免读取到旧值,也可以用锁保护他们

 

volatile 变量是相对于锁较弱的一种同步方式,如果将变量声明为 volatile 类型,那么编译器与运行时都不会优化相应的代码,不会将 volatile 变量缓存到寄存器或者其他对其他处理器不可见的地方,因此,每当读取 volatile 变量时,都可以保证返回的是最新写入的值

访问 volatile 变量不会加锁,也就不会让其他线程阻塞,但是使用 volatile 变量通常让代码变得更加脆弱,所以需要谨慎使用

下面是一个使用 volatile 的例子:

volatile boolean asleep; while (!asleep) { // do something }

 

 

这是一个通过 volatile 变量控制轮训的代码,虽然这段代码看上去并没有加锁的代码容易理解,当其他线程陷入睡眠而更新这个变量,我们的线程就会立即检测到新的值而退出循环执行后面的代码

需要注意的是,如果使用两个 volatile 控制状态,你就无法保证条件的不变性了,而且如果需要先判断当前值再写入新值,此时判断结果将不再可靠,除非你能保证只有一个线程会修改这个 volatile 变量,而其他线程对这个变量只读

 

上面我们已经看到如果没有线程同步机制,将有可能发生灾难性的后果,避免非线程安全造成的数据访问问题还有另一种解决办法,那就是不共享

这种技术被称为线程封闭,是最简单的线程安全实现方式

即使在封闭空间中使用非线程安全的对象,他依然是线程安全的

 

Ad-hoc 线程封闭

ad-hoc 线程封闭是由程序实现来控制多个线程不共享数据,将某个特定的子系统实现为一个单线程子系统,这样的情况下,单线程子系统提供了一个简便的线程安全模型

但 Ad-hoc 线程封闭技术是脆弱的,所以应该尽量少使用他

 

栈封闭

栈封闭指的是所有线程需要的数据都在方法局部创建在栈空间上,由于栈空间封闭在运行线程上,虽然堆空间是共享的,但是其他任何线程都无法获取到该线程对这部分空间的引用

使用栈封闭需要注意的是,程序员需要确保被引用的对象不会溢出

 

ThreadLocal 类

维持线程封闭的一种更规范的方法是使用 ThreadLocal 类,这个类能使线程中的某个值与保存值的对象关联起来

ThreadLocal 提供了 get 和 set 等方法来实现一个线程封闭的容器

 

JDBC 的 connection 是非线程安全的,但是我们又要避免在线程中反复创建连接,通常我们维护一个连接池来保存所有的连接,我们也可以使用 ThreadLocal 来保存一个对线程封闭的 Connection

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DBURL); } }; public static Connection getConnection() { return connectionHolder.get(); }

 

 

ThreadLocal 技术很适合维持一个线程隔离的缓冲区,同时,ThreadLocal 保存线程当前的配置也是非常有用的,这样就避免了连续调用很多方法,为每个方法都传递相同的配置信息

但是,如果滥用 ThreadLocal,就会降低代码的可重永兴,在类之间引入隐含的耦合

 

一旦被创建之后就不可以被修改的对象一定是线程安全的,通常我们可以创建没有属性或属性均为 final 的对象,这样只要在创建对象的过程中,this 引用没有逸出,他一定是线程安全的

 






技术帖      龙潭书斋      线程      thread      java      对象      object      concurrent      java并发编程实战      共享     


京ICP备15018585号