并发之ReentrantLock

920 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

与synchronized 具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized相同点,都支持可重入.

1 基本语法

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

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();
     }
}
/*
运行结果:
17:59:11.862 [main] c.TestReentrant - execute method1 
17:59:11.865 [main] c.TestReentrant - execute method2 
17:59:11.865 [main] c.TestReentrant - execute method3 
*/

3 可打断

ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
     log.debug("启动...");
     try {
     lock.lockInterruptibly();
     } catch (InterruptedException e) {
     e.printStackTrace();
     log.debug("等锁的过程中被打断");
     return;
     }
     try {
     log.debug("获得了锁");
     } finally {
     lock.unlock();
     }
    }, "t1");

    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
         sleep(1);
         t1.interrupt();
         log.debug("执行打断");
    } finally {
     lock.unlock();
    } 
/*
运行结果:
18:02:40.520 [main] c.TestInterrupt - 获得了锁
18:02:40.524 [t1] c.TestInterrupt - 启动... 
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException 
 at 
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898) 

*/

不可中断模式

使用了 interrupt 也不会让等待中断.

ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
         log.debug("启动...");
         lock.lock();
         try {
         log.debug("获得了锁");
         } finally {
         lock.unlock();
         }
    }, "t1");

    lock.lock();
    log.debug("获得了锁");
    t1.start();

    try {
         sleep(1);
         t1.interrupt();
         log.debug("执行打断");
         sleep(1);
    } finally {
     log.debug("释放了锁");
     lock.unlock();
    }
/*
运行结果:
18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动... 
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁
*/

4 锁超时

立刻失败

ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
         log.debug("启动...");
         if (!lock.tryLock()) {
         log.debug("获取立刻失败,返回");
         return;
         }
         try {
         log.debug("获得了锁");
         } finally {
         lock.unlock();
         }
    }, "t1");

    lock.lock();
    log.debug("获得了锁");
    t1.start();

    try {
     sleep(2);
    } finally {
     lock.unlock();
    }
/*
运行结果:
18:15:02.918 [main] c.TestTimeout - 获得了锁
18:15:02.921 [t1] c.TestTimeout - 启动... 
18:15:02.921 [t1] c.TestTimeout - 获取立刻失败,返回
*/

超时失败

ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
         log.debug("启动...");
         try {
         if (!lock.tryLock(1, TimeUnit.SECONDS)) {
         log.debug("获取等待 1s 后失败,返回");
         return;
         }
         } catch (InterruptedException e) {
         e.printStackTrace();
         }
         try {
         log.debug("获得了锁");
         } finally {
         lock.unlock();
         }
    }, "t1");

    lock.lock();
    log.debug("获得了锁");
    t1.start();

    try {
     sleep(2);
    } finally {
     lock.unlock();
    }
/*
运行结果:
18:19:40.537 [main] c.TestTimeout - 获得了锁
18:19:40.544 [t1] c.TestTimeout - 启动... 
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回

*/

使用tryLock解决哲学家就餐问题

筷子类

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);
     }
}

5 公平锁

ReentrantLock 默认是不公平的.

ReentrantLock lock = new ReentrantLock(false);
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();
/*
运行结果:
t42 running... 
t43 running... 
强行插入 start... 
强行插入 running... 
t44 running... 
t45 running... 
t46 running... 
*/

强行插入,有机会在中间输出(复现比较困难)

公平锁

创建对象时,将对象设置为true.

ReentrantLock lock = new ReentrantLock(true);

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();
/*
运行结果:
t485 running... 
t481 running... 
强行插入 running... 

*/

保证每次执行,且总在最后输出.公平锁一般没有必要,并且会降低并发度.

6 条件变量

synchronized 中也有条件变量,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的.

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

说明:

  • 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();
     }
}
/*
运行结果:
18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟
*/