ReentrantLock详解

ReentrantLock简介

都支持可重入,相对于 synchronized 它还具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量(可理解成多个WaitSet)

基本语法

// 获取锁(也可以放在try里面)
// lock 和 unlock 之间代码的都可以看作临界区
reentrantLock.lock();
try {
    // 临界区
} finally { 
    // 释放锁
    reentrantLock.unlock(); 
}

可重入

与 synchronized 一样,都支持可重入。可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

例如:

@Slf4j(topic = "c.Test22_1")
public class Test22_1 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        lock.lock();
        try {
            log.debug("enter main");
            m1();
        } finally {
            lock.unlock();
        }
    }

    private static void m1() {
        lock.lock();
        try {
            log.debug("enter m1");
            m2();
        } finally {
            lock.unlock();
        }
    }


    private static void m2() {
        lock.lock();
        try {
            log.debug("enter m2");
        } finally {
            lock.unlock();
        }
    }
}

/*Output:
17:38:41.749 c.Test22_1 [main] - enter main
17:38:41.759 c.Test22_1 [main] - enter m1
17:38:41.760 c.Test22_1 [main] - enter m2
*/

可打断

可打断指的是当线程竞争锁失败时,线程会进入阻塞状态(BLOCKED)等待锁释放

  • 此时如果是synchronized则不能被打断,会一直阻塞;
  • 而如果使用.lockInterruptibly()方法加锁,则可以打断处于阻塞状态的线程,打断后会抛出异常InterruptedException

注意,获得锁之后都可以被打断。

示例:

@Slf4j(topic = "c.Test22_2")
public class Test22_2 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争 --> 获取lock对象锁
                // 如果有竞争  --> 进入阻塞队列,但是可以被其他线程打断,不用继续等
                // 注意是在阻塞的时候被打断,即还没获得锁,不是在获得锁之后被打断
                log.debug("尝试获得锁");
                lock.lockInterruptibly(); // 可被打断锁
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断,没有获得到锁,返回");
                // 如果不返回会继续执行完本线程,这违背了锁的初衷
                return;
            }
            try {
                log.debug("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        // 主线程先加锁
        lock.lock();
        t1.start();

        Sleeper.sleep(1);
        log.debug("打断t1");
        t1.interrupt();

    }
}

/*Output:
18:46:44.618 c.Test22_2 [t1] - 尝试获得锁
18:46:45.618 c.Test22_2 [main] - 打断t1
18:46:45.621 c.Test22_2 [t1] - 等锁的过程中被打断,没有获得到锁,返回
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at cn.itcast.test.Test22_2.lambda$main$0(Test22_2.java:21)
	at java.lang.Thread.run(Thread.java:748)
*/

如果用.lock的话打断后没有任何反应,还会一直阻塞。

锁超时

在获取锁的过程中,如果其他线程持有着锁一直没有释放,尝试获取锁的线程不会一直阻塞,会立即(tryLock())或者等待一段时间(tryLock(long timeout, TimeUnit unit))放弃,如果在这段时间后仍然没有获得锁,会取消阻塞。

源码:

// 返回一个 Boolean 值,获取到锁则返回 true
// 可以看出这个方法也可以打断
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

例子:

@Slf4j(topic = "c.Test22")
public class Test22 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("t1尝试获得锁");
            try {
                // 等待2s
                if (! lock.tryLock(2, TimeUnit.SECONDS)) {
                    log.debug("t1获取不到锁");
                    // 获取不到锁,应该返回
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("被打断,t1获取不到锁");
                return;
            }
            try {
                log.debug("t1获得到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("主线程获得到锁");
        t1.start();
        // 睡眠1s
        sleep(1);
        log.debug("主线程释放了锁");
        lock.unlock();
    }

}

/*Output:
20:26:44.061 c.Test22 [main] - 主线程获得到锁
20:26:44.067 c.Test22 [t1]   - t1尝试获得锁
20:26:45.067 c.Test22 [main] - 主线程释放了锁
20:26:45.067 c.Test22 [t1]   - t1获得到锁
*/

解决哲学家思考问题

public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
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,成为锁对象
class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

/*Output:
不会产生死锁
*/

公平锁(待修改原理)

  • 公平锁(Fair):阻塞的线程在阻塞队列中争抢锁时,按照进入阻塞队列的先后顺序,先进入的先获得到锁
  • 非公平锁(Nonfair):阻塞的线程在阻塞队列中争抢锁时,一起竞争,不考虑进入阻塞队列的先后顺序

公平锁的意思就是先到先执行,本义是为了解决饥饿问题,但是用的不多,因为tryLock就可以解决饥饿问题,而且设置为公平锁会降低并发度。

synchronized是不公平的,ReentrantLock 默认也是不公平的,但可以设置成公平锁:

// 默认不开启公平锁
public ReentrantLock() {
    sync = new NonfairSync(); 
}
// 可以自行设置开启公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

源码角度分析

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 WaitSet 休息室,当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)需重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

使用方法:

private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {

    // 创建一个新的条件变量(休息室)
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();

    // 获得锁
    lock.lock();

    // 进入休息室1等待
    condition1.await();

    // 唤醒休息室1里的一个线程
    condition1.signal();
    // 唤醒休息室1里的所有线程
    condition1.signalAll();
}

示例:

@Slf4j(topic = "c.Test24")
public class Test24 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    // 锁对象
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室(条件变量1)
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室(条件变量2)
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        // 去等待烟的休息室等待
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        // 去等待外卖的休息室等待
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);

        new Thread(() -> {
            ROOM.lock();
            try {
                hasTakeout = true;
                // 相比 synchronized, 唤醒的范围缩小
                log.debug("外卖到了!");
                waitTakeoutSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
            ROOM.lock();
            try {
                hasCigarette = true;
                // 相比 synchronized, 唤醒的范围缩小
                log.debug("烟到了!");
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送烟的").start();
    }
}

/*Output:
22:40:42.005 c.Test24 [小南] - 有烟没?[false]
22:40:42.011 c.Test24 [小南] - 没烟,先歇会!
22:40:42.013 c.Test24 [小女] - 外卖送到没?[false]
22:40:42.013 c.Test24 [小女] - 没外卖,先歇会!
22:40:43.005 c.Test24 [送外卖的] - 外卖到了!
22:40:43.006 c.Test24 [小女] - 可以开始干活了
22:40:44.032 c.Test24 [送烟的] - 烟到了!
22:40:44.032 c.Test24 [小南] - 可以开始干活了
*/

与synchronized比较

以上介绍了Reentrantlock相比synchronized重要的四个特点以及都具有的可重入特性,在此想谈一下对这两种锁机制的思考。

  • synchronized的临界区(加锁的作用域)为代码块,所以执行完代码块会自动释放锁;而Reentrantlock需要用lockunlock方法手动加锁、释放锁。
  • 无论是synchronized还是Reentrantlock都维护着一段临界代码,在synchronized中获取不到锁就一直阻塞,临界区代码就不会被执行,不用考虑后续的问题;而Reentrantlock提供了打断和超时等选项,所以尤其要注意,当获取锁失败后(例如阻塞时打断或超时)一定要记得return,否则临界区代码有可能会继续执行,这违背了锁的初衷。
  • synchronized是对某一个对象加锁,释放锁只能等临界区代码执行完或进入WaitSet,而且不能被打断;Reentrantlock是对Reentrantlock对象加锁,所以Reentrantlock释放锁比较方便,可以在任意线程里调用unlock方法,而且提供了可被打断的锁实现方法.lockInterruptibly(),可打断这种机制也能解决死锁问题,但这是一种被动的解决死锁的方案。
  • synchronized获取锁失败一定会阻塞住,即进入加锁对象的EntrySet等待,这样容易产生死锁;而Reentrantlock提供了超时方法trylock, 这是一种主动的解决死锁的方案。
  • synchronized在对对象加锁后,关联的Monitor中只有一个WaitSet,而Reentrantlock可以新建多个,这样更易于管理,唤醒的精度更高。

你可能感兴趣的:(Java并发编程)