并发编程思考-线程安全性

原文链接: https://my.oschina.net/dokia/blog/833230

何为线程安全性?

线程安全性的对象是可变共享变量。“可变”表明历史上对此变量的访问在当下或未来不可信,“共享”表明此变量即使未被本线程过,也会有被其他线程改变的风险。非可变变量或非共享变量都不存在线程安全性问题。在并发应用中,对线程安全性最常见的需求是:某线程对可变共享变量的访问期间,此变量的状态不应被其他线程改变

这种严格的需求使变量在本线程的访问期间对其他线程而言不再具有可变性,从而保证在本线程的视角下,对此变量的访问等同于单线程状态下的变量访问,从而保证数据的准确性。

然而在另外一些并发应用中,需求却恰恰相反:其他线程对共享变量的设置状态,本线程根据不同的状态做出不同的行为。例如主线程的while循环判断共享变量flag,而子线程在一定情况下修改flag状态,从而使主线程退出while循环。

在这种情况下,上面对线程安全性的定义就不准确了。其实,何为“安全”,具体要由应用场景来判断。若由一句定义模糊的话来概括,则可总结为“某线程对可变共享变量的访问期间,此变量的状态表现出正确的行为”。然而要问什么是“正确的行为”,还应由具体场景下的具体规范来确定。比如某些场景下,需要将当前线程访问的共享变量状态对其他线程保持不变,在另一些场景下,当前线程要及时获得其他线程对共享变量状态的修改。

原子性与竞态条件

原子性是线程安全性中的一个重要概念。原子性意为“不可分割”,即在多线程情况下,某线程中的操作要么全部执行完成,要么完全不执行。其他线程无法在原子操作的开始至完成期间访问及修改共享变量的状态。这样一来,在原子操作过程中,共享变量对于其他线程来说是“不可更改”的,由此保证了线程安全性。

在实际应用中,很多非原子操作被当成了原子操作,从而造成线程安全问题。最著名的当属“计数器”问题,即i++问题。这个操作由“读取-修改-写入”三部分操作组成,并非原子操作。如果在无线程保护的情况下使用,极有可能在多个线程中读取同一个数值,然后分别写入相同的数值。从而造成计数误差。而造成错误的原因,是在并发操作中存在竞态条件,即根据一种可能失效的观察来判断或执行某计算。在非原子的操作组合中,上一步操作的结果对于下一步来说并不可信。如果不加线程保护,很有可能由于其他线程的干扰而产生错误的结果。

加锁机制

原子操作避免了线程安全问题。然而在一些逻辑复杂的场景中,无法避免使用非原子操作,或者原子操作的组合。这样就引入了加锁机制。在并发条件下,一个线程A进入加锁代码块时获得排它锁,然后进入加锁代码块,直到执行完毕,释放排它锁。在此期间,当其他线程想访问加锁代码块时,需等待线程A释放排它锁,竞争得到此锁,然后才能进入加锁代码块执行逻辑。这样通过加锁机制保证了复杂操作组合的可信度,从而保证了原子性。

然而,由于排它锁只有一把,所以各并行逻辑需要串行通过加锁代码块,这样就降低了并发应用的活跃性。所以加锁前要仔细考量。

转载于:https://my.oschina.net/dokia/blog/833230

你可能感兴趣的:(并发编程思考-线程安全性)