1. 为什么需要Lock
- 锁是一种工具,用于控制对共享资源的访问
Lock
和synchronized
,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的区别Lock
并不是用来代替synchronized
的,而是当使用synchronized
不合适或者不足以满足要求的时候,来提供高级功能的Lock
接口中最常见的实现类是ReentrantLock
- 通常情况下,
Lock
只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也允许并发访问,比如ReadWriteLoc
中的ReadLock
1.1 为什么Synchronized
不够用
① 效率低: 锁的释放情况少(执行完成或者出现异常),试图获得锁时不能设置超时,不能中断一个正在获得锁的线程
② 不够灵活: 加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的(读写锁更灵活)
③ 无法知道是否成功获得锁
1.2 而对于lock
呢
lock
作为一个接口,最典型的实现类是就是ReentrantLock
,lock中声明了四种获得锁的方法相对synchronized来说具备可中断
,可以设置超时时间
,可以设置为公平锁
,支持多个条件变量
等特点
1.lock():
lock()就是最普通的获取锁,如果锁已经被其他线程获取,则进行等待
lock()不会像synchronized一样在异常的时候自动释放锁
因此最佳实践是在finally中释放锁,以保证发生异常的时候锁一定被释放
lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待
2.tryLock():
tryLock()用来尝试获取锁,如果当前的锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
相比于lock(),这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
该方法会立即返回,即使拿不到锁也不会死等
3.tryLock(long, TimeUnit)
2. ReentrantLock
lock
接口最典型的实现类是就是ReentrantLock
,相对于 synchronized
它具备如下特点
① 可中断 (可以在取消等待锁,终止等待)被动的避免死等
对比synchronized
,synchronized
获得的锁不可中断,也就是说线程A一直在等待锁,可以让求他线程终止他的等待
② 可以设置超时时间,被动的避免死等
这个的意思是说,线程A等待锁,我们可以让大他再等待了一定时间后,如果还没等待的锁就放弃等待,可以避免死锁问题
③ 可以设置为公平锁 (先到先得,防止饥饿)
④ 支持多个条件变量(不满足的条件不同可以去不同的waitset
中等待,synchronized
只有一个waitset
)
⑤ 与 synchronized
一样,都支持可重入(同一个线程对同一个对象可以反复加锁)
2.1 正确使用姿势
lock
就是最普通的获得锁的方法,如果锁已经被其他锁获取了,则进行等待
上面说过相对与synchronized
不同,lock
并不会在异常的时候自动释放锁,所以为了保证不管是不是出现了异常都可以释放锁,把临界区的方法放入try
,释放锁的代码放入finally
块
//先创建ReentrantLock对象,在线程外
reentrantLock.lock();
try {
// 临界区
} finally {//为了保证不管是不是出现了异常都可以释放锁
// 释放锁
reentrantLock.unlock();
}
2.2 特性详解
① 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
② 可打断 lock.lockInterruptibly()
被动的避免死等
可打断的ReentrantLock
要使用lockInterruptibly
方法获得,被打断或获得InterruptedException
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
//可打断的ReentrantLock要使用lockInterruptibly方法获得
lock.lockInterruptibly();
//执行逻辑
try {
log.debug("获得了锁");
} finally {
//释放锁
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
}
}, "t1");
//以下是主线程的代码
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
注意如果是不可中断模式,那么即使使用了 interrupt
也不会让等待中断
③ 锁超时 lock.tryLock(1, TimeUnit.SECONDS)
主动的避免死等,使用的是tryLock
,不带参数就是尝试获得锁,获得不到直接放弃,tryLock
也支持可打断,源码的注释中给我们注明了它的正确使用姿势
//A typical usage idiom for this method would be:
Lock lock = ...;
//if判断尝试获得锁是否成功,成功才会进行if语句块进行临界区的操作,并且在finally释放锁,没拿到锁是不会执行unlock的
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
对哲学家问题的改进
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {//如果我没有获得右手筷子,那么会继续往下执行,释放了左手筷子,就避免了死锁
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
④ 公平锁
(按照进入阻塞队列的顺序获得锁)(可以解决饥饿)
ReentrantLock
默认是不公平的,不公平指的是,不会按进入阻塞队列的顺序去获得锁
可以通过构造方法改变为公平锁
ReentrantLock lock = new ReentrantLock(true);
public static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
lock.lock();
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
lock.unlock();
}
公平锁一般没有必要,会降低并发度
⑤ 条件变量
synchronized
中也有条件变量,就是我们讲原理时那个 waitSet
休息室,当条件不满足时进入 waitSet
等待
ReentrantLock
的条件变量比 synchronized
强大之处在于,它是支持多个条件变量的,这就就好比
synchronized
是那些不满足条件的线程都在一间休息室等消息- 而
ReentrantLock
支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒
使用要点:
- 通过
lock.newCondition()
获得新的条件变量 await
前需要获得锁 ,之后进入相应的休息室await
执行后,会释放锁,进入conditionObject
等待await
的线程被唤醒(或打断、或超时)取重新竞争 lock 锁- 竞争
lock
锁成功后,从await
后继续执行
static ReentrantLock lock = new ReentrantLock();
//获得新的条件
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
//进入相应的条件变量休息室等待
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
sleep(1);
sendBreakfast();
sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
3. 可见性保证
Lock
的加解锁和synchronized
有同样的内存语义,也就是说下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作