1.1 共享对象和同步(2)
图1-3是Counter对象的Java实现。该计数器由单线程调用时效果很好,但由多线程共享使用时却会出现错误。其问题的根本就在于语句

实质上是下面几行代码的缩写方式:

在这段代码中,value是对象Counter的一个域,它被所有的线程所共享。但是,每个线程都有一个它自己的本地拷贝temp,该拷贝是线程的局部变量。
假设线程A和线程B同时调用Counter的getAndIncrement()方法。那么,A和B有可能同时从value中读入1,然后分别将各自的局部变量temp设置为1,再将value设置为2,最终,A和B都返回了value的先前值1。显然,这种情形并不是我们所期望的:对计数器getAndIncrement()方法的并发调用返回了同一个值。我们希望不同的线程返回不同的值。事实上,还有可能出现更坏的情形:一个线程从value中读入了1,在它将value置为2之前,另一个线程执行了多次增量循环,读入1设置为2,接着读入2设置为3。当第一个线程最终完成其增量操作并将value设置为2时,它实际上是将value的值从3改回到2。

出现上述问题的根本原因在于对计数器值的增加需要在共享变量上执行两种不同类型的操作:将value的值读入temp变量,并将temp的值写入Counter对象。
类似的情形在现实生活中同样也会出现。当在走廊中行走时,需要躲过向你迎面走来的人。你可能发现那一瞬间自己一会儿向右,一会儿向左,这么来回好几次,因为另一个人也在试图这么做以避免与你碰撞。有时成功避开了,但有时还是撞上了。实际上,正如在后面的章节中将要讲述的那样,这种碰撞在很多时候是无法避免的。直观上来看,你和向你迎面走来的人都在做两件事:观察(“读”)对方当前的位置,然后移向(“写”)另一边。然而问题是,当你在读对方的位置时,无法知道他下一秒是继续待着还是移动躲闪。你和对面的这个陌生人必须决定谁从左边走谁从右边走。同样,共享计数器的各个线程也需要决定谁先使用谁后使用。
在第5章将会看到,现代的多处理器硬件通常都提供了特殊的读-改-写指令,允许线程以原子的硬件操作来读、写或修改存储器的值。对于上述的Counter对象,可以采用这种硬件方式原子地完成计数器值的自增。
同样,通过在软件(只使用读、写指令)中保证一个时刻只有一个线程在执行读/写操作序列,也可以获得这种原子的行为效果。这种确保一个时刻只允许一个线程执行特定代码段的问题称为互斥问题,它是多处理器程序设计中经典的协作问题之一。
在实际编程(www.cppentry.com)中并不需要由自己来设计互斥算法(而是调用库)。但是,深入地理解互斥算法的实现是从全局上掌握并发计算的基础。同样,对于死锁、有界公平性、不同于非阻塞方式的阻塞同步等这些普遍但很重要问题的具体解决方法,也需要进行深入的研究。