灵活性地提高带来了额外的责任。 缺少块结构锁定需要手动地去释放锁。 在大多数情况下,应使用以下惯用法:
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally {
lock.unlock();
}
当锁定和解锁发生在不同的范围内时,必须小心以确保通过try-finally或try-catch保护持有锁定时执行的所有代码,以确保在必要时释放锁定。
Lock实现通过使用非阻塞尝试获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly以及尝试获取锁),提供了比使用synchronized方法和语句更多的功能。可能会超时( tryLock(long, TimeUnit) )。
Lock类还可以提供与隐式监视器锁定完全不同的行为和语义,例如保证顺序,不可重用或死锁检测。 如果实现提供了这种特殊的语义,则实现必须记录这些语义。
请注意, Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。 获取Lock实例的监视器锁与调用该实例的任何lock方法没有指定的关系。 建议避免混淆,除非在自己的实现中使用,否则不要以这种方式使用Lock实例。
4.内存同步
======
所有Lock实现必须强制执行与内置监视器锁所提供的相同的内存同步语义,如Java语言规范中所述 :
-
一个成功的lock操作具有同样的内存同步效应作为一个成功的锁定动作。
-
一个成功的unlock操作具有相同的存储器同步效应作为一个成功的解锁动作。
不成功的锁定和解锁操作以及可重入的锁定/解锁操作不需要任何内存同步效果。
实施注意事项
锁获取的三种形式(可中断,不可中断和定时)在其性能特征可能有所不同。 此外,在给定的Lock类中,可能无法提供中断正在进行的锁定的功能。 因此,不需要为所有三种形式的锁获取定义完全相同的保证或语义的实现,也不需要支持正在进行的锁获取的中断。 需要一个实现来清楚地记录每个锁定方法提供的语义和保证。 在支持锁获取中断的范围内,它还必须服从此接口中定义的中断语义:全部或仅在方法输入时才这样做。
5.Lock提供的接口
===========
5.1 获取锁
=======
void lock(); // 获取锁。
-
最普通的的获取锁,如果锁被其他线程获取则进行等待
-
lock不会像synchro 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 nized一样在异常的时候自动释放锁
-
因此必须在finally中释放锁,以保证发生异常的时候锁一定被释放
注意:lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁、lock()就会陷入永久等待状态
5.2 获取中断锁
=========
void lockInterruptibly() throws InterruptedException;
除非当前线程被中断,否则获取锁。
获取锁(如果有)并立即返回。
如果该锁不可用,则出于线程调度目的,当前线程将被挂起,并在发生以下两种情况之一之前处于休眠状态:
-
该锁是由当前线程获取的;
-
其他一些线程中断当前线程,并支持锁定获取的中断。
如果当前线程:在进入此方法时已设置其中断状态;要么获取锁时被中断,并且支持锁获取的中断,然后抛出InterruptedException并清除当前线程的中断状态。
注意事项
在某些实现中,中断锁获取的能力可能是不可能的,并且如果可能的话可能是昂贵的操作。 程序员应意识到可能是这种情况。 在这种情况下,实现应记录在案。与正常方法返回相比,实现可能更喜欢对中断做出响应。Lock实现可能能够检测到锁的错误使用,例如可能导致死锁的调用,并且在这种情况下可能引发(未经检查的)异常。
注意 synchronized 在获取锁时是不可中断的
5.3 尝试获取锁
=========
boolean tryLock();
非阻塞获取锁(如果有)并立即返回true值。 如果锁不可用,则此方法将立即返回false值。相比于Lock这样的方法显然功能更加强大,我们可以根据是否能获取到锁来决定后续程序的行为
注意:该方法会立即返回,即便在拿不到锁的时候也不会再一只在那里等待
该方法的典型用法是:
Lock lock = new ReentrantLock();
if(lock.tryLock()){
try{
// TODO
}finally {
lock.unlock();
}
}else{
// TODO
}
5.4 在一定时间内获取锁
=============
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
如果线程在给定的等待时间内获取到锁,并且当前线程尚未中断,则获取该锁。
如果锁可用,则此方法立即返回true值。 如果该锁不可用,则出于线程调度目的,当前线程将被挂起,并处于休眠状态,直到发生以下三种情况之一:
-
该锁是由当前线程获取的。
-
其他一些线程会中断当前线程,并支持锁定获取的中断。
-
经过指定的等待时间如果获得了锁,则返回值true 。
如果经过了指定的等待时间,则返回值false 。 如果时间小于或等于零,则该方法将根本不等待。
注意事项
在某些实现中,中断锁获取的能力可能是不可能的,并且如果可能的话可能是昂贵的操作。 程序员应意识到可能是这种情况。 在这种情况下,实现应记录在案。与正常方法返回或报告超时相比,实现可能更喜欢对中断做出响应。Lock实现可能能够检测到锁的错误使用,例如可能导致死锁的调用,并且在这种情况下可能引发(未经检查的)异常。
5.5 解锁
======
void unlock(); //释放锁。
注意事项
Lock实现通常会限制哪些线程可以释放锁(通常只有锁的持有者才能释放锁),并且如果违反该限制,则可能引发(未经检查的)异常。
5.6 获取等待通知组件
============
Condition newCondition(); //返回绑定到此Lock实例的新Condition实例。
该组件与当前锁绑定,当前线程只有获得了锁。 才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
注意事项
Condition实例的确切操作取决于Lock实现。
5.7总结
=====
Lock对象锁还提供了synchronized所不具备的其他同步特性,如可中断锁的获取(synchronized在等待获取锁时是不可中断的),超时中断锁的获取,等待唤醒机制的多条件变量Condition等,这也使得Lock锁具有更大的灵活性。Lock的加锁和释放锁和synchronized有同样的内存语义,也就是说下一个线程加锁后可以看到前一个线程解锁前发生的所有操作。
6.锁的分类
======
根据一下6种情况可以区分多种不同的锁,下面详细介绍
6.1要不要锁住同步资源
============
是否锁住锁名称实现方式例子锁柱悲观锁synchronized、locksynchronized、lock不锁住乐观锁CAS算法原子类、并发容器
悲观锁又称互斥同步锁,互斥同步锁的劣势:
-
阻塞和唤醒带来的性能劣势
-
永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环,死锁等活跃性问题
-
优先级反转
悲观锁:
当一个线程拿到锁了之后其他线程都不能得到这把锁,只有持有锁的线程释放锁之后才能获取锁。
乐观锁:
自己才进行操作的时候并不会有其他的线程进行干扰,所以并不会锁住对象。在更新的时候,去对比我在修改期间的数据有没有人对他进行改过,如果没有改变则进行修改,如果改变了那就是别人改的那我就不改了放弃了,或者重新来。
开销对比:
-
悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁的时间哪怕越来越长,也不会对互斥锁的开销造成影响
-
悲观锁一开始的开销比乐观锁小,但是如果自旋时间长,或者不停地重试,那么消耗的资源也会越来越多
使用场景:
-
悲观锁:适合并发写多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免,大量的无用自旋等消耗
-
乐观锁:适合并发读比较多的场景,不加锁能让读取性能大幅度提高
6.2能否共享一把锁
==========
是否共享锁名称可以共享锁(读锁)不可以排他锁(独占锁)



