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的你。