使用ReentrantLock和Lambda表达式让同步更纯净

最近我在读Javin Paul的一篇文章,是关于synchronized和ReentrantLock的区别的[注1]。文章强调了后者的优势,但是也保留了一些缺点,笨重的try-final代码块需要谨慎使用。

在赞同他的观点同时,我还存留着一些困惑。每当涉及到同步时,我都会想起这些困惑——对于这两种方法都不能对问题进行单独分析,因为同步是将同步的内容封装到函数里面的,这就不能对一些问题分别测试了。

为了探讨这个问题,我采用了以前尝试过的一个方法。尽管这种时候我不喜欢编程的部分。主要是我不大喜欢冗长的匿名类。但是现在有了Java8和Lambda表达式,就值得一试了。所以我用了Javin Paul的例子中“计数器”的部分写了一个测试用例,并开始重构。起初的代码是这样的:

class Counter {

  private final Lock lock;

  private int count;

  Counter() {
    lock = new ReentrantLock();
  }

  int next() {
    lock.lock();
    try {
      return count++;
    } finally {
      lock.unlock();
    }
  }
}

可以清楚地看到,try-final那丑陋的代码块给实际的函数带来了很多杂乱的东西[注2]。解决方法:将这段代码块封装在单独的类中,作为同步的一部分,并对外提供增加同步代码的方法。下面展示了新创建的Operation接口,以及它如何使用Lambda表达式[注3]:

class Counter {

  private final Lock lock;

  private int count;

  interface Operation<T> {
    T execute();
  }

  Counter() {
    lock = new ReentrantLock();
  }

  int next() {
    lock.lock();
    try {
      Operation<Integer> operation = () -> { return count++; };
      return operation.execute();
    } finally {
      lock.unlock();
    }
  }
}

在下面的类提取步骤中,声明了Syschronizer类,以确保给的Opreation在同步的范围内进行了适当的操作:

class Counter {

  private final Synchronizer synchronizer;

  private int count;

  interface Operation<T> {
    T execute();
  }

  static class Synchronizer {

    private final Lock lock;

    Synchronizer() {
      lock = new ReentrantLock();
    }

    private int execute( Operation<Integer> operation ) {
      lock.lock();
      try {
        return operation.execute();
      } finally {
        lock.unlock();
      }
    }
  }

  Counter() {
    synchronizer = new Synchronizer();
  }

  int next() {
    return synchronizer.execute( () -> { return count++; } );
  }
}

如果没错的话,这应该是作为一个初始类。测试顺利通过了,虽然JUnit测试在并发方面的测试不全面,但最后的一点修改至少保证了在单元测试的并发中顺序是合理的。

public class Counter {

  final Synchronizer<Integer> synchronizer;
  final Operation<Integer> incrementer;

  private int count;

  public Counter( Synchronizer<Integer> synchronizer ) {
    this.synchronizer = synchronizer;
    this.incrementer = () -> { return count++; };
  }

  public int next() {
    return synchronizer.execute( incrementer );
  }
}

如此以来,OperationSyschronizer都被移动到了单独的文件中。通过这种方式,同步方面的性能提高了,并且可以分开进行单元测试了。Counter类现在使用了构造函数来传入一个Syschronizer实例[注4]。此外,添加操作独立封装成”incrementer”。但是测试时,final值没有是公开的。为了避免违背原则,使用Mockito的办法对Syschronizer进行优化以确保合适的调用:

@Test
public void synchronization() {
    Synchronizer<Integer> synchronizer = spy( new Synchronizer<>() );
    Counter counter = new Counter( synchronizer );

    counter.next();

    verify( synchronizer ).execute( counter.incrementer );
  }

鉴于单元测试和测试用例之间的紧密耦合,通常我不过分退出调用方法验证。但如果有紧急情况,这样做也不算太坏。在此,我仅仅做了Java 8和Lambda表达式的一个热身活动,可能还忽略了并发性的内容——你觉得的呢?

注:

  1. Java的ReentrantLock示例,synchronized和ReentrantLock之间的不同之处,Javin Paul,2013年3月7日。
  2. 因为我第一个版本的失败,这些乱七八糟的东西使我心烦。
  3. 我决定返回一个参数来替代int,这样,同步机制就可以更好地重用。但是我不确定在这里由于性能或者其他原因,自动装箱会不会不加判断的重用,所以对于一个通用的方法,这里还有很多要考虑的地方,这就不是本文所涉及的范围了……
  4. 修改构造函数最不可能的目的可能就是,向默认的构造器引入一个Syschronized的示例,像this( new Syschronized() );,但是出于测试目的,这是可以接受的。
原文链接: javacodegeeks 翻译: ImportNew.com - 赖 信涛
译文链接: http://www.importnew.com/11585.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 赖 信涛

(了解我更多,在:赖信涛的个人网站

查看赖 信涛的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

2 条评论

  1. miracle1919 说道:

    interface Operation<T> {
    格式乱了

    Thumb up 0 Thumb down 0

跳到底部
返回顶部