reentrantLock和synchronized区别

synchronized和reentrantLock都能实现互斥同步,保证临界区代码块的线程安全。

synchronized语法

synchronized(锁对象){

        //临界区代码
}

synchronized在java层面上只是个关键字。当代码执行到synchronized(锁对象)时,Java会向底层申请一个管程(Monitor)对象,并关联到锁对象的对象头中(这里暂时不考虑默认加的是偏向锁)。当临界区代码执行完毕后就会自动释放锁,即使临界区代码抛出异常,也不会影响到锁释放。

reentrantLock语法

Lock lock = new ReentrantLock();

lock.lock();

try{
      //临界区代码

}finally {

      lock.unlock();

}

reentrantLock其实是一个java层面的类,该类提供了api来获取与释放锁。

如果他俩仅仅是语法层面的区别,那谁还会用reentrantLock呢(语法更复杂)?

使用reentrantLock的优势

其实该类不仅仅提供了获取、释放锁的方法,还提供了一些方法,实现synchronized不能实现的功能。

场景1 tryLock

reentrantLock重载了两个tryLock,一个是无参的,另一个是带时间和时间单位的。这两个方法都返回boolean,代表获得锁成功还是失败。

如果你将lock.lock()改成lock.tryLock(),那么当前线程(执行这段代码的线程)如果发现lock锁已经被其他线程占用了,不会进入阻塞,而是直接返回false,代表获得锁失败。当然,如果lock锁处于空闲状态,自然就成功获得锁返回true。总而言之,tryLock()方法只会让当前线程尝试一次性获得锁,获取不到也不阻塞。

你想想,synchronized和lock.lock()办不到吧!

再来看看tryLock(long, Timeunit)

这个方法也是用来获取锁的,只是会有一个超时的时间,代表在超时时间范围内如果成功获得锁就返回true,否则返回false。如果你不希望获得锁时至尝试那一刻,也不想因为锁长时间被占用而一直阻塞,你就可以用这个方法,传入你能接受的最长时间,过时不侯。

总结:synchronized只要获取不到锁就一直等待,直到获得锁为止。reentrantLock提供的tryLock方法可以用更灵活的方式去尝试获得锁,而不会死等。

场景2 lockInterruptibly

这也是一种获得锁方式的补充。假设有如下场景

现在有两个线程,一个小明,一个妈妈

小明需要买🍭来吃,但是今天买🍭的很多需要排队,当轮到小明时才能开始吃

伪代码模拟小明线程:

lock.lock();//排队,等待轮到自己

try {
      //付钱,吃糖

} finally {

      //走人,释放锁

}

妈妈对小明说:“你先在这儿排着,我去买菜”。妈妈心想,要是我买完菜你还没有买到🍭,那必须回家。

伪代码模拟妈妈线程

//买菜

state == terminated//判断小明线程是否未终止

else  小明线程.interrupt()//叫小明该回家了

问题来了,妈妈线程发出的interrupt命令真的有效果吗?其实lock.lock()方法是不能有效相应中断指令的,其源码用的是park让线程阻塞,interrupt确实可以打断park,但是内部的处理逻辑大致是,小明收到了妈妈的打断信号,并立即将中断标志清除,再次判断当前🍭队列(到我了吗?)。如果没有,小明线程仍然会进入park,就因为小明主动清除了中断标记,第二次park是有效果的。

补充一下:LockSupport.park()内部会判断当前线程是否有中断标记,如果有,这行代码是不生效的,也不会阻塞住。

所以妈妈的呼唤小明是听到了,但是没有实际的效果。

!!!如果将lock.lock()替换成lock.lockInterruptibly()就能完美解决该问题,因为该方法内部park被中断后,直接抛出InterruptedException,小明线程自然也就不会继续等待。

场景3 支持多个条件变量

什么是条件变量呢?如果你了解过synchronized中锁对象.wait()的原理,那你其实已经知道什么是条件变量了。它就是Monitor中的WaitSet

简单说说wait/notify吧

如果你在同步代码块中调用了锁对象的wait方法,当前线程立马进入WAITING或者TIMED_WAITING状态,直到其他线程调用了锁对象的notify/notifyAll后等待的线程才会重新进入Runnable状态。其实wait方法底层会将调用wait方法的线程放在Monitor对象的WaitSet中,以便notify的时候能够找到这个线程池,这个WaitSet你就可以理解成java中的Set集合。Monitor对象只有一个这样的Set,notify的时候自然就会去这个WaitSet中随机挑一个去唤醒,而reentrantLock支持多个条件变量,具体几个由我们决定。

怎么创建一个条件变量

Lock lock = new ReentrantLock();

Condition c1 = lock.newCondition();

c1就是一个条件变量,所以一个条件变量一定是和一个reentrantLock对象关联的,由于newCondition方法可以多次调用,自然就可以创建多个条件变量。

Condition接口提供了类似wait、notify的API,就是await和signal。

那我何时可以调用c1.await和c1.signal呢,调用后有什么效果?

首先这两个方法只有在持有 c1关联的那把锁 的情况下才能调用(也就是同步代码块中能调)。我们刚刚说WaitSet就是一个Set集合,条件变量就类似于WaitSet,也是一个容器。其实看ConditionObject的源码可是,其底层存储线程的数据结构是链表。

调用c1.await就代表将当前线程暂停住,并将线程对象维护在c1对象的链表中。当其他线程调用c1.signal时,就会去唤醒c1内部链表中随机一个线程。但是值得注意的是,reentrantLock的优势就在于它支持多个条件变量,也就是如果调用c2.signal一定不会唤醒因为调用c1.await而阻塞的那些线程。这如果条件变量使用恰当,可以完全避免synchronized中锁对象.notify带来的虚假唤醒。

场景4 支持公平锁与非公平锁

reentrantLock内部维护了两个内部累FairSync和NonfairSync,分别表示公平锁和非公平锁,至于其具体实现咱先不谈,因为这牵扯到了AQS类。reentrantLock有个成员变量sync,在调用其构造器时初始化的。sync变量的运行时类就决定缔造出的的reentrantLock的公平性。

1.如何创建非公平锁(建议使用非公平锁)

其实ReentrantLock的无参构造器默认创建的就是非公平锁。

如果你不闲麻烦,也可以调用含一个boolean形参的构造器,传入false,就会创建非公平锁

2.如何创建公平锁(不建议使用公平锁)

使用用含一个boolean形参的构造器,传入true,就会创建公平锁

以上就是,synchronized和reentrantLock的区别,后续我还会出reentrantLock的锁超时、可打断、公平锁、可重入的实现原理,希望能够帮助到正在学习juc的你。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值