计算机的高速发展,在多核技术上要远远快于提升单核的计算能力。因而设计并发的程序成为提高软件性能的一大利器。
并发的程序虽然可以有效利用硬件资源,但同时也会增加程序设计的难度,其首要解决的就是同步的问题。
同步问题归纳而言就是要解决两个问题:活性失败(liveness failure)和 安全性失败(safety failture)。
- 活性失败是指,线程A操作的变量c,在线程B中要访问的时候,不是最新的线程A操作赋值后的值。产生此类问题的原因在于现代CPU多采用了高速缓存,高速缓存变成了CPU和内存的中间桥梁,数据的过渡器,而CPU对高速缓存中的数据的修改并不会第一时间刷新到公用的内存中;多个线程运行在不同的CPU的情况下,就有可能出现读取的数据的不新鲜,导致活性失败。
- 安全性失败,举例来说,线程A在调用某个计算方法, 运算过程处于中间状态时,有参与运算的变量被其他线程修改了值,导致了线程A的这次运算结果错误。这里的问题在于,线程A的这次运算不是一个原子操作,无法保证在运算的整体过程中数据的可控性。此类问题被称作安全性失败。
解决的办法归纳起来也是两大类
- 程序设计时,尽量减少跨线程的数据交互,尽量设计可重入的计算方法。
- 使用各种编程语言提供的同步机制,保证数据在多个线程之间的正确同步。
对于第一点来说,无论你是用哪种语言,都是一样的。
那么,针对第二点,让我们来看一下C#和Java在处理同步上的一些大同小异。
解决活性失败,C#和Java都提供了volatile这个关键字来修饰变量。这个关键字可以让程序运行时对被修饰的变量无条件的在高速缓存和主内存中实现数据同步。使用这个关键字可以解决数据不新鲜的问题,但是切记不可乱用,因为它会带来额外的性能开销,让高速缓存变得没有意义。
解决活性失败,当然也可以使用各种同步机制,这些同步机制也可以让需要在一起完成的操作不被其他线程打扰,成为原子操作,从而解决安全性失败问题。
同步大体来说可以分为两种
- 互斥同步。互斥同步是指,线程A在访问某个竞争资源的时候,其他线程不能访问这个资源而被阻塞。这种方案带来的问题是比较大的性能开销用于线程阻塞和唤醒。这种同步机制其实是一种悲观的同步方案,在操作开始前就假设会有其他线程来抢资源而上锁了。
- 非阻塞同步。这种同步机制是借助了操作和冲突检测的硬件指令实现的原子操作,实现的乐观同步机制。通俗的说,就是先进行操作,如果没有其他线程在征用共享数据,那操作就成功了,如果产生了冲突,那就不断重试,直到资源被释放。非阻塞同步不会让线程挂起,不需要被唤醒,所以如果在共享资源被短期暂用的情况下,比互斥(阻塞)同步拥有更好的性能。
在C#和Java中,比较典型的非阻塞同步机制就是自旋锁。
而同步机制的实现机制则是五花八门。
由于C#主要应用的平台在于windows,所以基本上它的同步机制都是基于windows的一些同步原语,包括事件,互斥锁,信号量,监视器;当然也有优化后的读写锁,瘦锁等等。
Java由于是跨平台的,所以提供的同步机制都需要jvm支持。可供选择的同步机制和封装有synchronized, ReentrantLock,CountDownLatch, CyclicBarrier, DelayQueue, PriorityBlockngQueue, ScheduledExecutor, Semaphore, Exchanger.
后续我们单独对每种实现进行相应的比较。