Lock
doc
- Lock实现提供了比使用synchronized 同步方法和语句块获得的更广泛的锁操作。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的条件(Condition)对象。
- 锁(Lock)是一种工具,用于控制多个线程对共享资源的访问。通常,Lock提供对共享资源的独占访问:一次只有一个线程可以获取锁,而对共享资源的所有访问都需要先获取锁。但是,有些锁可能允许并发访问共享资源,例如读写锁的读锁(ReadWriteLock)。
- synchronized 同步方法和语句块的使用提供了对与每个对象相关联的隐式监视器(Monitor)锁的访问,但是强制所有锁的获取和释放都以块结构的方式进行:当获取多个锁时,它们必须以相反的顺序释放(依次获取AB锁,此时就得先释放B再释放A),并且所有锁都必须在它们所在的同一个作用域中被释放。
- 虽然synchronized 修饰方法和语句块的作用域机制使使用监视器锁进行编程变得更加容易,并有助于避免许多涉及锁的常见编程错误,但在某些情况下,您需要以更灵活的方式使用锁。例如,一些用于遍历并发访问的数据结构的算法需要使用“hand over hand”或“chain locking”:(Lock特点)先获取节点A的锁,然后获取节点B,然后释放A并获取C,然后释放B并获取D,依此类推。锁接口的实现允许在不同的范围内获取和释放一个锁,并允许以任何顺序获取和释放多个锁,从而允许使用这些技术。
- 随着灵活性的增加,责任也随之增加。块结构锁的缺失消除了同步方法和语句发生的锁的自动释放,(synchronized 底层会自动释放锁,lock就得我们自己手动去释放)。在大多数情况下,应使用以下习惯用法:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
//访问受此锁保护的资源
} finally {
l.unlock();
}
- 当锁定和解锁发生在不同的范围内时,必须注意确保在锁定期间执行的所有代码都受到try finally或try catch的保护,以确保在必要时释放锁。
- Lock实现通过提供非阻塞的获取锁的尝试(tryLock())、获取可中断的锁的尝试(lockInterruptly)以及获取可能超时的锁的尝试(tryLock(long,TimeUnit)),从而提供了超过同步方法和语句的附加功能。
- Lock类还可以提供与隐式监视Monitor锁完全不同的行为和语义,例如保证排序、不可重入使用或死锁检测。如果一个实现提供了这种专门的语义,那么该实现必须记录这些语义。
- 请注意,锁实例只是普通对象,它们本身可以用作synchronized语句中的目标。获取锁实例的监视器monitor锁与调用该Lock实例的任何锁方法没有任何的关系。为了避免混淆,建议不要以这种方式使用锁实例,除非在它们自己的实现中。
内存同步
所有锁实现都必须执行与内置监视器Monitor锁提供的相同的内存同步语义,如Java语言规范(17.4内存模型)中所述:
- 成功的锁定操作与成功的锁定操作具有相同的内存同步效果。
- 成功的解锁操作与成功的解锁操作具有相同的内存同步效果。
- 不成功的锁定和解锁操作以及可重入的锁定/解锁操作不需要任何内存同步效果。
锁获取的三种形式(可中断、不可中断和定时)在性能特征、排序保证或其他实现质量方面可能有所不同。此外,在给定的锁类中,中断正在进行的锁获取的能力可能不可用。因此,实现不需要为所有三种形式的锁获取定义完全相同的保证或语义,也不需要支持正在进行的锁获取的中断。需要一个实现来清楚地记录每个锁定方法提供的语义和保证。它还必须遵守此接口中定义的中断语义,以支持锁获取的中断程度:要么完全中断,要么只在方法入口中断。
由于中断通常意味着取消,并且对中断的检查通常很少,所以实现可以优先响应中断而不是常规方法返回。即使可以显示在另一个操作之后发生的中断可能已经解除了线程的阻塞,这也是正确的。实现应该记录这种行为。
/**
* 获取锁。
* 如果锁不可用,则当前线程将因线程调度而被禁用,并处于休眠状态,直到获得锁为止。 *
*/
void lock();
/**
当前线程被中断,获取锁
获取锁(如果可用)并立即返回。
如果锁不可用,则当前线程将因线程调度而被禁用,并处于休眠状态,直到发生以下两种情况之一:
1. 锁被当前线程获取;或者
(获取锁正常返回)
2.其他线程中断当前线程,并支持当前线程在获取锁时中断.
如果当前线程:
在进入此方法时设置其中断状态;或获取锁时中断,支持锁获取中断,
然后抛出InterruptedException并清除当前线程的中断状态。
(意思睡眠时其他线程中断了当前线程获取锁直接清除当前线程睡眠状态)
*/
void lockInterruptibly() throws InterruptedException;
/**
只有在调用时它是空闲的时才获取锁。 (意思锁可能拿不到 lock是一定能拿得到)
获取锁(如果可用),并立即返回值true。如果此方法不可用,则该方法将立即返回false。
此方法的典型用法是:
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
这种用法确保锁在被获取时被解锁,而在未获得锁时不尝试解锁。
*/
boolean tryLock();
/**
tryLock重载方法
如果锁在给定的等待时间内空闲并且当前线程没有中断,则获取该锁。
如果指定的等待时间为false,则返回的值为false。如果时间小于或等于零,则该方法根本不会等待。
time–等待锁定的最长时间
unit–时间参数的时间单位
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
释放锁。
注意:
锁实现通常会对线程释放锁施加限制(通常只有锁的持有者才能释放锁),如果违反了限制,
则可能会抛出(未检查的)异常。任何限制和异常类型都必须由该锁实现记录。
*/
void unlock();
/**
返回绑定到此Lock实例的新条件实例。
在等待条件之前,锁必须由当前线程持有。打电话给Condition.await() 将在等待之前自动释放锁,
并在等待返回之前重新获取锁。
施注意事项
条件实例的确切操作取决于锁实现,并且必须由该实现记录。
Condition 实现 wait notify 的功能 并且功能更强大
*/
Condition newCondition();
ReentrantLock 可重入锁(重要)
ReentrantLock示例
public class MyTest1 {
private Lock lock = new ReentrantLock(); //可重入锁
public void myMethod1() {
try {
lock.lock(); //获取不到锁一直等待直到获取锁
System.out.println("mymethod1 invoked");
} finally {
lock.unlock(); //释放锁 如果不释放其他线程就获取不到锁
}
}
public void myMethod2() {
// try {
// lock.lock(); //拿不到锁还一直卡住,直到获取到锁
// System.out.println("mymethod2 invoked");
// } finally {
// lock.unlock();
// }
boolean result = false;
try {
result = lock.tryLock(500, TimeUnit.MICROSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (result){
System.out.println("mymethod2 invoked");
lock.unlock(); //拿到锁就要释放
}
else {
System.out.println("can‘t get the loke "); //此时拿不到锁会执行这个语句
}
}
public static void main(String[] args) {
MyTest1 myTest1 = new MyTest1();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
myTest1.myMethod1();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
myTest1.myMethod2();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Lock与synchronized锁处理的差别
- 1.锁的获取方式: 前者是通过程序代码由开发者手工获取,后者是通过JVM来获取的(无需开发者干预)
- 2.具体实现方式: 前者是通过Java代码的方式来实现的,后者是通过JVM底层来实现(无需开发者关注)
- 3.锁的释放方式: 前者务必通过unlock() 方法在finally块中手动释放,后者是通过JVM来释放(无需开发者关注)
- 4.锁的具体类型: 前者提供了多种,如公平锁,非公平锁,后者与前者均提供了可重入锁
- synchronized底层使用的是互斥锁 是一种非公平锁
Condition
doc
Condition将对象监视器(monitor)方法(wait、notify和notifyAll)分解为不同的对象,通过将它们与任意Lock实现结合使用,为每个对象提供多个等待集合的效果。当锁Lock代替同步方法和语句的使用时,Condition代替对象监视器(monitor)方法的使用(我们可以把lock等同synchronized ,Condtion的方法当做wait和notify, 功能更加强大)。
传统对象等待集合只有一个 waitSet Lock可以通过newCondition()方法 生成多个等待集合Condition对象。 Lock和Condition 是一对多的关系
条件(也称为条件队列或条件变量)提供了一种方法,使一个线程暂停执行(以“wait”),直到另一个线程通知某些状态条件现在可能为true。由于对共享状态信息的访问发生在不同的线程中,因此必须对其进行保护,因此某种形式的Lock与Condition相关联。等待条件提供的关键属性是它原子性地释放关联的锁并挂起当前线程,就像对象。等等.(意思就是Condition必须和Lock连用 await才使用 如果不连用 wait找不到对象锁)
Condition实例本质上绑定到Lock。要获取特定Lock实例的Condition实例,请使用其newCondition()方法
例如,假设我们有一个支持put和take方法的有界缓冲区。如果尝试在空缓冲区上执行take,则线程将阻塞,直到某个项可用(队列中存在元素可用);如果在完全缓冲区(元素满了)上尝试put,则线程将阻塞,直到缓冲区有空间可用为止。我们希望保持等待-放入线程,并将线程放入不同的等待集中,这样当缓冲区中的项或空间可用时,我们可以使用仅通知单个线程的优化。这可以通过使用两个Condition实例来实现。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
(在java.util.concurrent.ArrayBlockingQueue类提供此功能,因此没有理由实现此示例用法类。)
condition实现可以提供不同于对象monitor方法的行为和语义,例如保证通知的顺序,或者在执行通知时不需要持有锁。如果一个实现提供了这种专门的语义,那么该实现必须记录这些语义。
请注意,condition实例只是普通对象,它们本身可以用作synchronized语句中的目标,并且可以调用它们自己的monitor等待和通知方法。获取条件实例的监视器锁或使用其监视器方法,与获取与该条件相关联的锁或使用其等待和信令方法没有指定关系。为了避免混淆,建议您不要以这种方式使用条件实例,除非在它们自己的实现中。
除非另有说明,否则为任何参数传递null值都将导致抛出NullPointerException。
一个实现可以自由地消除虚假唤醒的可能性,但建议应用程序程序员始终假定它们可能发生,因此总是在循环中等待(while循环。 await方法必须被 当前condition实例的signal 或signalAll唤醒)。
condition等待的三种形式((interruptible 可中断, non-interruptible 不可中断, and timed 和定时)在某些平台上的实现难度和性能特征可能有所不同
/**
使当前线程等待,直到发出siganal信号或中断。
与此condition相关联的Lock将被自动释放,当前线程出于线程调度目的被禁用并处于休眠状态,
直到发生以下四种情况之一:
1,另一个线程为此condition调用signal方法,并且当前线程恰好被选为要唤醒的线程;
2,其他线程为此condition调用signalAll方法
3.其他线程中断当前线程,支持线程挂起中断
4.出现“虚假唤醒”。
跟wait一样被唤醒还是的竞争锁竞争到了才能执行await之后的代码
如果当前线程:在进入此方法时设置其中断状态;或等待时中断,支持线程挂起中断,
然后抛出InterruptedException并清除当前线程的中断状态。
在第一种情况下,没有规定是否在释放锁之前进行中断测试。
*/
void await() throws InterruptedException;
/**
与await 相同 不可被中断
与此condition相关联的Lock将被自动释放,当前线程出于线程调度目的被禁用并处于休眠状态,
直到发生以下四种情况之一:
1,另一个线程为此condition调用signal方法,并且当前线程恰好被选为要唤醒的线程;
2,其他线程为此condition调用signalAll方法
4.出现“虚假唤醒”
*/
void awaitUninterruptibly();
/**
使当前线程等待,直到发出siganal信号或中断,或指定的等待时间过去。
1,另一个线程为此condition调用signal方法,并且当前线程恰好被选为要唤醒的线程;
2,其他线程为此condition调用signalAll方法
3.其他线程中断当前线程,支持线程挂起中断
4.出现“虚假唤醒”。
5.指定的时间到了,会被唤醒
如下
nanos = theCondition.awaitNanos(500);
nanos = 300
意思就是 等待200纳秒就被唤醒
返回值为500-200 返回值是估值 近似 其次该等待时间使用纳秒为了更准确
如果返回时给定提供的nanoTimeout值,则该方法返回要等待的纳秒数的估计值,如果超时,
则返回小于或等于零的值。此值可用于确定在等待返回但等待的条件仍不成立的情况下是否重新
等待以及重新等待多长时间。典型的方法有以下几种:
boolean aMethod(long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);//转成纳秒
lock.lock();
try {
while (!conditionBeingWaitedFor()) {
if (nanos <= 0L) // <=0 时 返回超时了
return false;
nanos = theCondition.awaitNanos(nanos);
}
// ...
} finally {
lock.unlock();
}
}
Params:nanoTimeout–等待的最长时间,以纳秒为单位
return:nanoTimeout值的估计值减去从该方法返回时所花费的时间。正值可以用作后续调用此方法以完成等待所需时间的参数。小于或等于零的值表示没有时间剩余。
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
使当前线程等待,直到发出信号或中断,或指定的等待时间过去。这种方法在行为上等同于:
awaitNanos(unit.toNanos(time)) > 0
* @param 等待的最长时间
* @param 时间参数的时间单位
* @return 如果超过等待时间,则为false,否则为true
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/*
使当前线程等待,直到发出信号或中断,或指定的截止时间过去。
boolean aMethod(Date deadline) {
boolean stillWaiting = true;
lock.lock();
try {
while (!conditionBeingWaitedFor()) {
if (!stillWaiting)
return false;
stillWaiting = theCondition.awaitUntil(deadline);
}
// ...
} finally {
lock.unlock();
}
}
Params 截止日期–等待的绝对时间
return 如果返回时已过截止日期,则为false,否则为true
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
唤醒一个等待的线程。
如果有多个线程正在此condition实例中等待,则选择一个线程进行唤醒。
然后,该线程必须在从await返回之前重新获取锁。(意思就是被唤醒还是的竞争锁)
signal调用必须在lock 获取 和锁释放之间
*/
void signal();
/**
唤醒所有等待的线程。
如果有线程在此condition实例下等待,那么它们都将被唤醒。每个线程必须重新获取锁,然后才能从await返回。(意思就是被唤醒还是的竞争锁)
signalAll调用必须在lock 获取 和锁释放之间
*/
void signalAll();
有界缓冲区
示例:
假设我们有一个支持put和take方法的有界缓冲区。如果尝试在空缓冲区上执行take,则线程将阻塞,直到某个项可用;如果在完全缓冲区上尝试put,则线程将阻塞,直到有空间可用为止。我们希望保持等待-放入线程,并将线程放入不同的等待集中,这样当缓冲区中的项或空间可用时,我们可以使用仅通知单个线程的优化。这可以通过使用两个条件实例来实现。
class BoundedContainer {
private String[] elements = new String[10];
private Lock lock = new ReentrantLock();
private Condition notEmptyCondition = lock.newCondition();
private Condition notFullCondition = lock.newCondition();
int takeIndex, putIndex, elementCount;
@SneakyThrows
public void put(String element) {
this.lock.lock();
try {
while (this.elementCount == this.elements.length) {
notFullCondition.await();
}
elements[putIndex] = element;
if (++putIndex == this.elements.length) {
this.putIndex = 0;
}
++elementCount;
System.out.println("put method " + Arrays.toString(elements));
notEmptyCondition.signal();
} finally {
lock.unlock();
}
}
@SneakyThrows
public String take() {
this.lock.lock();
try {
while (this.elementCount == 0) {
notEmptyCondition.await();
}
String outIndex = elements[takeIndex];
elements[takeIndex] = null;
if (++takeIndex == this.elements.length) {
this.takeIndex = 0;
}
--elementCount;
System.out.println("take method " + Arrays.toString(elements));
notFullCondition.signal();
return outIndex;
} finally {
lock.unlock();
}
}
测试
public static void main(String[] args) {
BoundedContainer container = new BoundedContainer();
// 包含左不包含右
IntStream.range(0, 10)
.forEach(i -> new Thread(() -> container.put("hell0")).start());
IntStream.range(0, 10)
.forEach(i -> new Thread(() -> container.take()).start());
}
此时打印如下
put method [hell0, null, null, null, null, null, null, null, null, null]
put method [hell0, hell0, null, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0]
take method [null, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0]
take method [null, null, hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0]
take method [null, null, null, hell0, hell0, hell0, hell0, hell0, hell0, hell0]
take method [null, null, null, null, hell0, hell0, hell0, hell0, hell0, hell0]
take method [null, null, null, null, null, hell0, hell0, hell0, hell0, hell0]
take method [null, null, null, null, null, null, hell0, hell0, hell0, hell0]
take method [null, null, null, null, null, null, null, hell0, hell0, hell0]
take method [null, null, null, null, null, null, null, null, hell0, hell0]
take method [null, null, null, null, null, null, null, null, null, hell0]
take method [null, null, null, null, null, null, null, null, null, null]
此时先有序put再进行take 我们把put和take调换位置
public static void main(String[] args) {
BoundedContainer container = new BoundedContainer();
// 包含左不包含右
IntStream.range(0, 10).forEach(i -> new Thread(() -> container.take()).start());
IntStream.range(0, 10).forEach(i -> new Thread(() -> container.put("hell0")).start());
}
}
此时打印
put method [hell0, null, null, null, null, null, null, null, null, null]
put method [hell0, hell0, null, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, null, null, null, null, null]
take method [null, hell0, hell0, hell0, hell0, null, null, null, null, null]
take method [null, null, hell0, hell0, hell0, null, null, null, null, null]
take method [null, null, null, hell0, hell0, null, null, null, null, null]
take method [null, null, null, null, hell0, null, null, null, null, null]
put method [null, null, null, null, hell0, hell0, null, null, null, null]
put method [null, null, null, null, hell0, hell0, hell0, null, null, null]
take method [null, null, null, null, null, hell0, hell0, null, null, null]
put method [null, null, null, null, null, hell0, hell0, hell0, null, null]
put method [null, null, null, null, null, hell0, hell0, hell0, hell0, null]
take method [null, null, null, null, null, null, hell0, hell0, hell0, null]
put method [null, null, null, null, null, null, hell0, hell0, hell0, hell0]
take method [null, null, null, null, null, null, null, hell0, hell0, hell0]
take method [null, null, null, null, null, null, null, null, hell0, hell0]
take method [null, null, null, null, null, null, null, null, null, hell0]
此时日志是无序的 原因由于此时队列中没有数据 此时take线程集合就会进行等待 等待put线程进行添加数据当5个添加线程添加完成后take线程才竞争到锁,(注意 take线程集合和put线程集合分别存放到notEmptyCondition 和notFullCondition 当中) 由于put线程和take线程相同 结果 队列中肯定没有元素存在
public static void main(String[] args) {
BoundedContainer container = new BoundedContainer();
// 包含左不包含右
IntStream.range(0, 5).forEach(i -> new Thread(() -> container.take()).start());
IntStream.range(0, 10).forEach(i -> new Thread(() -> container.put("hell0")).start());
}
此时 打印如下
put method [hell0, null, null, null, null, null, null, null, null, null]
put method [hell0, hell0, null, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, null, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, null, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, null, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, null, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, null, null, null]
put method [hell0, hell0, hell0, hell0, hell0, hell0, hell0, hell0, null, null]
take method [null, hell0, hell0, hell0, hell0, hell0, hell0, hell0, null, null]
take method [null, null, hell0, hell0, hell0, hell0, hell0, hell0, null, null]
take method [null, null, null, hell0, hell0, hell0, hell0, hell0, null, null]
take method [null, null, null, null, hell0, hell0, hell0, hell0, null, null]
put method [null, null, null, null, hell0, hell0, hell0, hell0, hell0, null]
take method [null, null, null, null, null, hell0, hell0, hell0, hell0, null]
put method [null, null, null, null, null, hell0, hell0, hell0, hell0, hell0]
5个take 和10个put 此时最终结果
put method [null, null, null, null, null, hell0, hell0, hell0, hell0, hell0]
此时修改 如下
public static void main(String[] args) {
BoundedContainer container = new BoundedContainer();
// 包含左不包含右
IntStream.range(0, 10).forEach(i -> new Thread(() -> container.take()).start());
IntStream.range(0, 5).forEach(i -> new Thread(() -> container.put("hell0")).start());
}
打印如下
由于此时take线程大于put线程 当队列没有元素就会进入等待状态
当我们不进行take 只put 大于队列时
public static void main(String[] args) {
BoundedContainer container = new BoundedContainer();
// 包含左不包含右
// IntStream.range(0, 10).forEach(i -> new Thread(() -> container.take()).start());
IntStream.range(0, 15).forEach(i -> new Thread(() -> container.put("hell0")).start());
}
由于put完10个之后超过队列最大值 就会进入阻塞