0%

Java 并发 - Synchronized 与其他一些锁机制的对比

1.前言

我们前面已经 Synchronized 在JDK1.6 进行哪一些方面的优化,通过这些底层的优化之后Synchronized 变得好用了很多,那么它究竟和其他锁机制有什么差别?换种方式说,我在进行编程的过程中,究竟要如何进行选择?什么时候应该选择Synchronized?而什么时候不选用Synchronized而选用其他的锁机制。

总感觉学习了Synchronized之后,对于其内部原理熟悉了,不知道有没有其他人跟我一样困惑,我究竟该何时进行使用它呢?在哪个场景下我该第一时间想到这货?我觉得进行选择还应该先进行对比,将和我们之前学习到的一些同步机制进行联系起来,有对比才有总结,尽量多进行比较,多点思考,才有更深入的理解与认识。


2.区别分析

2.1 Synchronized 和 Reentrantlock(可重入锁)的联系与区别:

  • 总体而言:Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

    • 注意:ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
  • 联系

    • 两者都是可重入锁
      • “可重入锁” 概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
  • 区别

    • 锁的实现:synchronized 依赖于 JVM ,而 ReentrantLock 依赖于 API。
      • synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
    • 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。 性能已不是选择标准。
    • ReentrantLock 比 synchronized 增加了一些高级功能:
      1. 等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
      2. 公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
      3. 锁绑定多个条件:synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
  • 使用选择除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。


2.2 Synchronized 和 Lock 的联系与区别:

  • 总的来说:Synchronized是 Java 内置关键字在 Jvm 层面,Lock是个 Java 类。Lock有比 Synchronized 更精确的线程语义和更好的性能。Lock的锁定是通过代码实现的,而Synchronized是在 JVM 层面上实现的。
  • 区别
    • 是否可以获得锁状态:
      • synchronized无法判断是否获取锁的状态;
      • Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。
    • 是否自动释放锁:
      • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁);
      • Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
    • 是否阻塞:
      • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去;
      • 而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。它的tryLock方法可以非阻塞方式去拿锁。
    • 范围:
      • Lock锁的范围有局限性,块范围;
      • 而synchronized可以锁住块、对象、类。
  • 区别小结:synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  • 适用场景:Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

2.3 Synchronized 和 CAS 联系与区别?

  • 总括:简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)。
  • 使用场景选择
    • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
    • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
  • 补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。