Java 8新特性探究(十)StampedLock将是解决同步问题的新宠

Java8就像一个宝藏,一个小的API改进,也足与写一篇文章,好比同步,一直是多线程并发编程的一个老话题,相信没有人喜欢同步的代码,这会下降应用的吞吐量等性能指标,最坏的时候会挂起死机,可是即便这样你也没得选择,由于要保证信息的正确性。因此本文决定将从synchronized、Lock到Java8新增的StampedLock进行对比分析,相信StampedLock不会让你们失望。java

synchronized

在java5以前,实现同步主要是使用synchronized。它是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,可以保证在同一时刻最多只有一个线程执行该段代码。编程

有四种不一样的同步块:多线程

  1. 实例方法并发

  2. 静态方法工具

  3. 实例方法中的同步块性能

  4. 静态方法中的同步块优化

你们对此应该不陌生,因此很少讲了,如下是代码示例
this

synchronized(this)
// do operation
}

小结:在多线程并发编程中Synchronized一直是元老级角色,不少人都会称呼它为重量级锁,可是随着Java SE1.6对Synchronized进行了各类优化以后,性能上也有所提高。spa

Lock

它是Java 5在java.util.concurrent.locks新增的一个API。线程

Lock是一个接口,核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;

ReentrantReadWriteLock, ReentrantLock 和synchronized锁都有相同的内存语义。

与synchronized不一样的是,Lock彻底用Java写成,在java这个层面是无关JVM实现的。Lock提供更灵活的锁机制,不少synchronized 没有提供的许多特性,好比锁投票,定时锁等候和中断锁等候,但由于lock是经过代码实现的,要保证锁定必定会被释放,就必须将unLock()放到finally{}中

下面是Lock的一个代码示例

rwlock.writeLock().lock();
try {
// do operation
} finally {
rwlock.writeLock().unlock();
}

小结:比synchronized更灵活、更具可伸缩性的锁定机制,但无论怎么说仍是synchronized代码要更容易书写些

StampedLock

它是java8在java.util.concurrent.locks新增的一个API。

ReentrantReadWriteLock 在沒有任何读写锁时,才能够取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即若是执行中进行读取时,常常可能有另外一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,若是读取执行状况不少,写入不多的状况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃没法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字做为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被受权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操做不少,写的操做不多的状况下,你能够乐观地认为,写入与读取同时发生概率不多,所以不悲观地使用彻底的读取锁定,程序能够查看读取资料以后,是否遭到写入执行的变动,再采起后续的措施(从新读取变动信息,或者抛出异常) ,这一个小小改进,可大幅度提升程序的吞吐量!!

下面是java doc提供的StampedLock一个例子

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //得到一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其余写锁发生?
        stamp = sl.readLock(); //若是没有,咱们再次得到一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
//下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //若是成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //若是不能成功转换为写锁
           sl.unlockRead(stamp); //咱们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 而后再经过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }

小结:

StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。

StampedLock与ReadWriteLock性能对比

下图是和ReadWritLock相比,在一个线程状况下,是读速度其4倍左右,写是1倍。

下图是六个线程状况下,读性能是其几十倍,写性能也是近10倍左右:

下图是吞吐量提升:

总结

一、synchronized是在JVM层面上实现的,不但能够经过一些监控工具监控synchronized的锁定,并且在代码执行时出现异常,JVM会自动释放锁定;

二、ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定必定会被释放,就必须将unLock()放到finally{}中;

三、StampedLock 对吞吐量有巨大的改进,特别是在读线程愈来愈多的场景下;

四、StampedLock有一个复杂的API,对于加锁操做,很容易误用其余方法;

五、当只有少许竞争者的时候,synchronized是一个很好的通用的锁实现;

六、当线程增加可以预估,ReentrantLock是一个很好的通用的锁实现;

StampedLock 能够说是Lock的一个很好的补充,吞吐量以及性能上的提高足以打动不少人了,但并非说要替代以前Lock的东西,毕竟他仍是有些应用场景的,起码API比StampedLock容易入手,下篇博文争取更新快一点,可能会是Nashorn的内容,这里容许我先卖个关子。。。

相关文章
相关标签/搜索