java的reentrantlock和synchronize

what:

  reentrantlock参考文件:https://www.cnblogs.com/sfzlstudy/p/16337830.html

 

where:

  现在一般使用,建议:synchronize(隐式锁,自动释放锁);

  如果是对并发要求特别高的情况下,建议:reentrantlock(显式锁,需要手动释放锁);

 

  原因:ReentrantLock 和 Synchronized 最核心的区别就在于:

     Synchronized 适合于并发竞争低的情况,因为 Synchronized 的锁升级,如果最终升级为重量级锁在使用的过程中是没有办法消除的,意味着每次都要和 cpu 去请求锁资源。而 ReentrantLock 主要是提供了阻塞的能力(在state上),通过在高并发下线程的挂起,来减少竞争,提高并发能力。

 

why:

  最初版本中 ReentrantLock 的性能是远远强于 Synchronized 的,到 jdk1.6 之后,两种锁的性能已经相差无几,甚至 Synchronized 的自动释放锁会更好用。JDK8及后面的ConcurrentHashMap就不使用segment的ReentrantLock了,改用CAS+synchronize,具体见文章:https://www.cnblogs.com/sfzlstudy/p/16334418.html

 

  synchronize:

    a、synchronized 的使用是非常简单的,直接贴在方法和代码块上。程序运行期间,Synchronized 那一块儿代码发生么什么?来看一张图:

 

       在多线程运行过程中, 线程会去先抢对象的监视器 ,这个监视器对象独有的,其实就相当于一把钥匙,抢到了,那你就获得了当前代码块的执行权。

其他没有抢到的线程会进入队列 (SynchronizedQueue) 当中等待,等待当前线程执行完后,释放锁。

 

      (没太明白:)SynchronizedQueue 是一个比较特殊的队列,它没有存储功能,它的功能就是维护一组线程,其中每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其实没有任何一个元素,或者说容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用 peek 操作,因为只有移除元素时才有元素。

举个例子:

喝酒的时候, 先把酒倒入酒盅,然后再倒入酒杯,这就是正常的队列 。

喝酒的时候, 把酒直接倒入酒杯,这就是 SynchronizedQueue 。

这个例子应该很清晰易懂了,它的好处就是可以直接传递,省去了一个第三方传递的过程。

    

    b、锁升级

       jdk1.6 以前,Synchronized 是一个重量级锁(由JVM升级CPU锁)。原因:cpu 的锁数量是固定的 ,当 cpu 锁资源使用完后还会进行锁等待,这是一个非常耗时的操作。

      在 jdk1.6后,优化为“无锁->偏向锁->自旋锁->CPU的重量级锁”。

        偏向锁: 相当于给对象贴了一个标签 (将自己的线程 id 存入对象头中),下次我再进来时,发现标签是我的,我就可以继续使用了。

        自旋锁:徘徊等待,发现可以用,就占用。

 

      注意:锁也有将锁操作。HotSpot 虚拟机中是有锁降级的, 但是仅仅只发生在 STW 的时候 ,只有垃圾回收线程能够观测到它,也就是说, 在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。

 

 

  reentrantlock:

    a、使用也是非常简单的,与 Synchronized 的不同就是需要自己去手动释放锁,为了保证一定释放,所以通常都是和 try~finally 配合使用的。

    b、依赖AQS实现。AQS由“独占共享”两种模式。reentrantlock是独占模式。

      独占模式的流程是比较简单的,根据: state 是否为 0 来判断是否有线程已经获得了锁,没有就阻塞,有就继续执行后续代码逻辑

 

       共享模式的流程根据: state 是否大于 0 来判断是否有线程已经获得了锁,如果不大于 0,就阻塞,如果大于 0,通过 CAS 的原子操作来自减 state 的值,然后继续执行后续代码逻辑。

 

 

 

 

 

  

diff:
  1、底层实现:

    synchronized 是 JVM 层面的锁,是 Java 关键字,通过 monitor 对象来完成(monitorenter 与 monitorexit),对象只有在同步块或同步方法中才能调 wait / notify 方法。synchronized 的实现涉及到锁的升级,具体为"无锁->偏向锁->自旋锁->向 OS 申请CPU的重量级锁"

    ReentrantLock 是从 jdk1.5 以来(java.util.concurrent.locks.Lock)提供的 API 层面的锁。ReentrantLock 实现则是依赖AQS,AQS底层是利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和 volatile 保证数据可见性以实现锁的功能。

 

  2、手动释放:

    synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用。

    ReentrantLock 则需要用户去手动释放锁,如果有手动释放锁,就可能导致死锁现象。一般通过 lock() 和 unlock() 方法配合 try / finally 语句块来完成,使用释放更加灵活。

 

  3、中断:

    synchronized 是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成

    ReentrantLock 则可以中断,可通过 trylock(long timeout,TimeUnit unit) 设置超时方法;或者将 lockInterruptibly() 放到代码块中,调用 interrupt 方法进行中断。

 

  4、公平锁:

    synchronized 为非公平锁。

    ReentrantLock 默认为非公平锁,但是可以选公平锁也可以选非公平锁。

 

  5、绑定条件condition:

    synchronized 不能绑定。

    ReentrantLock 通过绑定 Condition 结合 await() / singal() 方法实现线程的精确唤醒,而不是像 synchronized 通过 Object 类的 wait() / notify() / notifyAll() 方法要么随机唤醒一个线程要么唤醒全部线程

  

  6、锁的对象:

    synchronzied 锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁 / 争抢锁

    ReentrantLock 锁的是线程,根据进入的线程和 volatile的int 类型的 state 标识锁的获得 / 争抢。

 

posted @ 2022-06-02 16:03  修心而结网  阅读(99)  评论(0编辑  收藏  举报