synchronized和ReentrantLock有什么区别?

典型回答

synchronized是Java内建的同步机制,它提供了互斥的语义和可见性。当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在Java 5以前,synchronized是仅有的同步手段。在代码中,synchronized可以用来修饰方法,也可以使用在特定的代码块上,本质上synchronized方法等同于把方法全部语句用synchronized块包起来。

ReentrantLock,通常翻译为再入锁,是Java 5提供的锁实现,它的语义和synchronized基本相同。再入锁通过代码直接调用lock()方法获取,代码书写也更加方便。与此同时,ReentrantLock提供了很多实用的方法,能够实现很多synchronized无法做到的细节控制,比如可以控制fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须明确调用unlock()方法释放锁。

synchronized和ReentrantLock的性能不能一概而论,早期版本synchronized在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于ReentrantLock。

知识扩展

1、理解线程安全

线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性。这里的状态反映在程序中其实可以看作是数据。换个角度,如果状态不是共享的,或者不是可修改的,也就不存在线程安全问题。由此得到两个保证线程安全的办法:

  • 封装:通过封装我们可以将对象内部状态隐藏、保护起来。
  • 不可变:还记得本专栏之前讲到的final和immutable吗?Java语言目前还没有真正意义上的原生不可变,但是未来也许会引入。

线程安全需要保证几个基本特性:

  • 原子性,简单说就是相关操作不会中途被其它线程干扰。一般通过同步机制实现。
  • 可见性,是一个线程修改了某个共享变量,其状态能够立即被其它线程知晓。通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
  • 有序性,是保证线程内串行语义,避免指令重排等。

可能有点晦涩,那么看看下面的代码段。这个例子通过取两次数值然后进行对比,来模拟两次对共享状态的操作。你可以编译并执行,可以看到,仅仅是两个线程的低度并发,就非常容易碰到former和latter不相等的情况。这是因为,在这两次取值的过程中,其它线程可能已经修改了sharedState。

public class ThreadSafeSample {
  public int sharedState;
  public void nonSafeAction() {
    while (sharedState < 100000) {
      int former = sharedState++;
      int latter = sharedState;
      if (former != latter - 1) {
        System.out.println(“Observed data race, former is " + former
          + ", " + “latter is " + latter);
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    ThreadSafeSample sample = new ThreadSafeSample();
    Thread threadA = new Thread() {
      public void run() {
        sample.nonSafeAction();
      }
    };
    Thread threadB = new Thread() {
      public void run() {
        sample.nonSafeAction();
      }
    };
    threadA.start();
    threadB.start();
    threadA.join();
    threadB.join();
  }
}

下面是在我的电脑上的运行结果:

> java ThreadSafeSample
Observer data race, former is 14290, latter is 14292

如果你将两次赋值过程用synchronized保护起来,使用this作为互斥单元,就可以避免别的线程并发地去修改sharedState。

synchronized (this) {
  int former = sharedState++;
  int latter = sharedState;
}

2、设置公平性

再来看看ReentrantLock。你可能好奇什么是“再入”?它表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念。也就是说,锁的持有是以线程为单位,而不是基于调用次数。Jave锁实现强调再入性是为了和pthread的行为进行区分。

再入锁可以设置公平性(fairness),我们可以在创建再入锁时选择是否是公平的。

ReentrantLock fairLock = new ReentrantLock(true);

这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。

如果使用synchronized,我们根本无法进行公平性的选择,其永远是不公平的,这也是主流操作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java默认的调度策略很少会导致“饥饿”发生。与此同时,若要保证公平性则会引入额外开销,自然导致一定的吞吐量下降。所以,建议只有当你的程序确实有公平性需要的时候,才有必要指定它。

ReentrantLock相比synchronized,因为可以像普通对象一样使用,所以可以利用其提供的各种便利方法,进行精细的同步操作,甚至是实现synchronized难以表达的用例,如:

  • 带超时的获取锁尝试。
  • 可以判断是否有线程,或者某个特定线程,在排队等待获取锁。
  • 可以响应中断请求。
  • ……

3、条件变量

如果说ReentrantLock是synchronized的替代选择,java.util.concurrent.locks.Condition则是将wait、notify、notifyAll等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。

举例,设计这样一个阻塞的缓冲区,它支持put和take方法。如果试图在空的缓冲区上执行take操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行put操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待set中保存put线程和take线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition实例来做到这一点。

public class BlockingBuffer {
  final Lock lock = new ReentrantLock();
  final Condition notFull = lock.newCondition();
  final Condition notEmpty = lock.newCondition();

  final Object[] items = new Object[100];
  int putptr, takeptr, count;

  public void put(Object x) throws InterruptedException {
    lock.lock();
    try {
      while (count == items.length)
        notFull.await();
      items[putptr] = x;
      if (++putptr == items.length) putptr = 0;
      ++count;
      notEmpty.signal();
    } finally {
      lock.unlock();
    }
  }

  public Object take() throws InterruptedException {
    lock.lock();
    try {
      while (count == 0)
        notEmpty.await();
      Object x = items[takeptr];
      if (++takeptr == item.length) takeptr = 0;
      --count;
      notFull.signal();
    } finally {
      lock.unlock();
    }
  }
}

调用await方法将导致当前线程在接到信号或被中断之前一直处于等待状态;而调用signal方法则意味着唤醒一个等待线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值