Lock
避免死锁
可能发生死锁的条件中不可剥夺条件
指的是
线程已经获得资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放!
要想破坏这个条件,就需要具有申请不到资源,就释放已占有资源的能力
!使用synchronized,如果线程申请不到资源就会进入阻塞状态
,做什么也改变不了它的状态,这是synchronized的致命弱点!
可以从下面三个方面来解决synchronized的局限(破坏不可剥夺条件):
特性 | 描述 | API |
---|---|---|
能响应中断 | 如果不能自己释放,可以响应中断跳出阻塞等状态 | lockInterruptibly() |
非阻塞式的获取锁 | 尝试获取,获取不到不会阻塞,直接返回false | tryLock() |
支持超时 | 如果一段时间没获得锁,不是进入阻塞状态而是返回false | tryLock(time, timeUnit) |
显式锁Lock除了能完成synchronized关键字的功能外,还能通过显式锁对象提供的方法查看哪些线程被阻塞了;可以创建Condition
对象进行线程间通信
;可以中断由于获取锁而被阻塞的线程;设置获取锁的超时时间!
Lock与synchronized
synchronized | Lock | |
---|---|---|
释放锁 | 隐式锁,出了作用域自动释放 | 显示锁(手动开启和关闭锁)必须手动释放锁 |
类型 | 内置的Java关键字 | Java接口 |
获取状态 | 无法判断获取锁的状态 | 可以判断是否获取到了锁 |
是否等待 | 线程1获得锁,线程2阻塞一直等待 | 不一定会等待(lock.tryLock()) |
重入/公平 | 可重入锁,不可以中断的,非公平 | 可重入锁,可以中断,是否公平可以自己设置 |
适用场景 | 适合锁少量的代码同步问题 | 适合锁大量的同步代码 |
Lock接口
public interface Lock {
/**
* 尝试获取锁
* 1.如果此刻锁未被其他线程持有,会立即返回,并设置锁的hold计数为1;
* 2.如果当前线程已经持有该锁,再次申请,hold计数加1,并立即返回;
* 3.如果该锁当前被另一个线程持有,那么线程会进入阻塞,直到获得该锁
* 注意:由于调用lock方法而进入阻塞状态的线程,同样不会被中断,会被放入AQS队列中阻塞挂起等待
*/
void lock();
/**
* Acquires the lock unless the current thread is interrupted.
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试非阻塞获取锁
* 如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
* 该方法不会引起当前线程阻塞。
*/
boolean tryLock();
/**
* 设置了超时时间,如果超时时间到了还没有获取到该锁则返回false
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* Releases the lock.
*
*/
void unlock();
/**
* 创建一个与该lock相关联的Condition对象
*/
Condition newCondition();
}
lock
public void lock()
获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。
如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一
直处于休眠状态,此时锁保持计数被设置为 1。
指定者:
接口 Lock 中的 lock
lockInterruptibly
public void lockInterruptibly() throws InterruptedException
1)如果当前线程未被中断,则获取锁。
2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态:
1)锁由当前线程获得;或者
2)其他某个线程中断当前线程。
5)如果当前线程获得该锁,则将锁保持计数设置为 1。
如果当前线程:
1)在进入此方法时已经设置了该线程的中断状态;或者
2)在等待获取锁的同时被中断。
则抛出 InterruptedException,并且清除当前线程的已中断状态。
6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。
指定者: 接口 Lock 中的 lockInterruptibly
抛出: InterruptedException 如果当前线程已中断。
tryLock public boolean tryLock()
仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS)
,它几乎是等效的(也检测中断)。
2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。
3)如果锁被另一个线程保持,则此方法将立即返回 false 值。
指定者:
接口 Lock 中的 tryLock
返回:
如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true;否则返回false
Lock使用范式
Lock lock = new ReentrantLock();
lock.lock();
// 在try{}外面获取锁
try {
// ...
} finally {
lock.unlock(); // 在finally中释放锁,目的是保证在获取到锁之后,最终能被释放
}
在 try{}
外获取锁主要考虑两个方面:
- 如果没有获取到锁就抛出异常,最终释放锁肯定是有问题的,因为还未曾拥有锁谈何释放锁呢;
- 如果在获取锁时抛出了异常,也就是当前线程并未获取到锁,但执行到
finally
代码时,如果恰巧别的线程获取到了锁,则会被释放掉。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 创建50个线程更新counter
*/
public class TestLock {
public static void main(String[] args) throws InterruptedException {
final int[] counter = {0};
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 50; i++) {
new Thread(
() -> {
lock.lock();
try {
int a = counter[0];
counter[0] = a + 1;
} finally {
lock.unlock();
}
}
).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println(counter[0]); // 50
}
}
Lock是怎样起到锁的作用
使用synchronized时,在程序编译成CPU指令后,在临界区会有moniterenter
和moniterexit
指令的出现,可以理解成进出临界区的标识。
锁类型 | 获取锁 | 释放锁 |
---|---|---|
synchronized | JVM指令monitor enter | JVM指令monitor exit |
Lock | lock() | unlock() |
Lock和synchronized关键字一样都具备可重入性,Lock内部维护一个hold计数器,而synchronized内部则维护了monitor计数器。若成功获取锁的初始值为1,那么持有该锁时再次获取锁,除了会立即成功外,对应的计数器也会随之自增。
无论是synchronized关键字还是Lock,其主要作用都是保证代码指令的原子操作。
在ReentrantLock内部维护了一个volatile修饰的变量state,通过CAS来进行读写(最底层还是交给硬件来保证原子性和可见性),如果CAS更改成功,即获取到锁,线程进入到try代码块继续执行;如果没有更改成功,线程会被【挂起】,不会向下执行。
但Lock是一个接口,里面根本没有state这个变量的存在。它怎么处理这个state呢?
Lock接口的实现类基本都是通过【聚合】了一个
队列同步器AQS
的子类完成线程访问控制的!