Java并行程序基础 --- 多线程及容错性处理

1. Java中提供的多线程编程手段

1.1 JDK5以前

  • 只有synchronized一种

1.2 JDK5到如今:

  • Atomic
  • Lock
  • AtomicStampedReference
  • ReentrantLock

2 多线程编程容易产生的问题

2.1 共享状态

你们都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程当中,势必涉及到数据的读取和写入。因为程序运行过程当中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,因为CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU比起来要慢的多,所以若是任什么时候间对数据的操做都要经过和内存的交互来进行,会大大下降指令执行的速度。所以在CPU里面就有了高速缓存html

也就是,当程序在运行过程当中,会将运算须要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就能够直接从它的高速缓存读取数据和向其中写入数据,当运算结束后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,好比下面的这段代码:编程

i=i+1

当线程执行这个语句时,会先从主存当中读取i的值,而后复制一份到高速缓存当中,而后CPU执行指令对i进行加1操做,而后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。缓存

这个代码在单线程中运行是没有任何问题的,可是在多线程中运行就会有问题了。在多核CPU中,每条线程能够运行于不一样的CPU中,所以每一个线程运行时有本身的高速缓存(对单核CPU来讲,其实也会出现这种问题。只不过是以线程调度的形式来分别执行的)。本文咱们以多核CU为例。安全

下图中,线程1和线程2共享i,并同时对i进行i++操做,可能存在这种状况,最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。多线程

为了解决缓存不一致问题,一般来讲有如下2种解决方法:框架

  • 经过在总线加Lock#锁的方式
  • 经过缓存一致性协议

这两种方式都是硬件层面上提供的方式。性能

在早期的CPU当中,是经过在总线加Lock#锁的形式来解决缓存不一致的问题。由于CPU和其余部件进行通讯都是经过总线来进行的,若是对总线加Lock#锁的话,也就是说阻塞了其余CPU对其余部件访问(如内存),从而使得只有一个CPU能使用这个变量的内存。好比上面例子中,若是一个线程在执行i=i+1过程当中,在总线上发出了LOCK#锁的信号,那么只有等待这段代码彻底执行完毕以后,其余CPU才能从变量i所在的内存读取变量。这样就解决了缓存不一致的问题。优化

可是上面的方式会有一个问题,因为在锁定总线期间,其余CPU没法访问内存,容易致使如下的Starvation(饥饿)和DeadLock(死锁)问题。spa

因此就出现了缓存一致性协议。最出名的就是Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。它的核心思想是:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存中从新读取。线程

2.2 Starvation(饥饿)

  • 高优先级的线程可能会占用全部CPU资源使得低优先级线程没法运行
  • 线程可能由于其余线程的竞争而没法进入一个同步块
  • Notify方法唤醒的老是其余线程以致于等待线程老是在sleep状态

2.3 DeadLock(死锁)

线程1锁定A,尔后尝试锁定B,线程2锁定B,尔后尝试锁定A,形成两个线程同时等待对方释放锁,进入死锁状态。

总结:多线程编程容易产生问题的根本缘由在于锁

3. 锁的不一样类型(详情请看《锁优化的思路和方法》的第二部分)

3.1 偏向锁

3.2 轻量级锁

3.3 自旋锁(Spinlock)

不用切换至内核态,可能会更高效

3.4 互斥锁(Mutex)或信号量

不占用CPU资源
会切换至内核态

4. Volatile

  • 一个线程写,另一个线程读取,能够保证共享变量的可见性
  • 当两个线程同时读写共享变量,Volatile无效
  • Volatile的读写会引发缓存内容失效,形成每一次读写均须要从主内存读取。所以性能会受到影响

4.1 正确使用volatile

你只能在有限的一些情形下使用volatile变量替代锁。要是volatile变量提供理想的线程安全,必须同时知足下面两个条件:

  • 对变量的写操做不依赖于当前值。
  • 对变量没有包含在具备其余变量的不变式中。

实际上,这些条件代表,volatile变量不能用做线程安全计数器。虽然增量操做(x++)看起来相似一个单独操做,实际上它是一个有读取-修改-写入操做序列组成的组合操做必须以原子方式执行,而volatile不能提供必须的原子性操做。

简单局其中使用一例:状态标志

也许实现volatile变量的规范使用仅仅是使用一个boolean状态标志,用于指示发生了一个重要的一次性时间,例如完成初始化或请求停机。

不少应用程序包含了一种控制结构,形式为"在尚未准备好中止程序时再执行一些工做"。如如下代码所示,将volatile变量做为状态标志使用:

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

极可能会从循环外部调用shutdown()方法 -- 即在另外一个线程中 -- 所以,须要执行某种同步来确保正确实现shutdownRequested变量的可见性。相对于synchronized,这里很是适合使用volatile。

5. 互斥锁和信号量

  • Java中的互斥锁是synchronized或者Lock,而信号量是Semaphore。
  • 互斥锁能够看做是信号量的一个特例:能够把互斥锁比做家里的卫生间,每次只能一我的进入,信号量比做是公共卫生间,有几个隔间就能够进入。
  • Synchronized是reentrant(可重入)的,即两个对同一个变量synchronized的方法a、b,一个线程能够在a内进入b。
  • Lock不是reentrant的,若是须要reentrancy,使用ReentrantLock。
  • Semaphore没有reentrancy的概念,Semaphore只关心还有多少空间,不论当前线程是否已经占有了空间,只要当前线程进入Semaphore,就会形成Semaphore的计数-1。
  •  
  • Java除有锁编程以外,同时支持原子变量,即本人另外一篇博客《Java并行程序基础 --- 无锁》的内容。
  • 原子变量是无锁编程,能够看做是volatile的扩展
  • 原子变量在volatile的基础上,保证了对CAS(compare and swap)操做的支持,容许多个线程对同一个原子变量读写操做。
  • 原子变量不能保证ABA问题,即一个变量的原始值为A,被修改成B,后又被修改成A,这时其余线程没法得知当前的A实际上是通过两次修改后的值
  • 对于ABA问题,能够加入版本号,经过AtomicStampedReference支持。

总结:

  • 根据前面讲述的多线程编程技巧,咱们可以经过无锁编程解决共享变量的问题,可是无锁编程须要比有锁编程更高的技巧,使得程序更加复杂,同时一旦发生多线程引发的问题,更加难以排查。
  • 而有锁编程引入了deadlocl和starvation的问题

那么如何解决starvation的问题?

引入公平锁

  • 因为公平锁加入了更加复杂的上锁机制,以及保证了每次唤醒的均是不一样的线程,因此公平锁的效率比普通的锁更低(可是会高于直接的synchronized)。
  • 经过公平锁能够避免因为线程始终不被唤醒形成的starvation,可是没法避免其余两种状况形成的starvation。
  • 因此最好的多线程编程,实际上是不上锁,而两个高效的使用多线程的处理框架Disruptor以及Akka,均采用了不上锁的方式,尽管方法不一样。
     

参考:http://www.importnew.com/18126.html

参考:http://www.cnblogs.com/shangxiaofei/p/5564047.html

参考:炼数成金--多线程及容错性处理

相关文章
相关标签/搜索