ReentrantLock 与 synchronized对比
最近有在阅读Java并发编程实战这本书,又看到了ReentrantLock和synchronized的对比,发现自己以前对于RenntrantLock的理解很片面,特此做一番总结,如果有总结不到位的,欢迎指出
java.util.concurrent.locks
接口 Lock
为什么需要Lock?
java.util.concurrent.locks
接口 Lock
Lock提供了一种如条件的、可轮询的、定时的以及可以终端的获取锁的操作,所有的加锁方式和解锁方式都是显式的。
public class Lock { private boolean locked = false; public Lock() { } public final synchronized void lock() throws InterruptedException { while(this.locked) { this.wait(); } this.locked = true; } public final synchronized void unlock() { this.locked = false; this.notifyAll(); } }
Lock是JAVA5.0出现的,它的出现并不是为了替代synchronized,而是在synchronized不适用的时候使用。
那么synchronized有什么局限性呢?
- 无法中断一个正在获取锁的线程
当一个线程想获取已经被其他线程持有的锁时,就会发生堵塞,假设已经持有锁的线程一直不释放锁,那么线程就会一直等待下去。
- 无法指定获得锁的等待时间
比如,想要A线程执行某个操作,想在指定时间内A线程没有获取到锁就返回。synchronized是做不到的。
相同点:
- 独占锁: 一次只允许一个线程访问
- 可重入锁: 一个线程可重复获得自己已获得锁,不会发生死锁。简单来说,递归的时候不会发生死锁
不同点:
- Lock不是java内置的,synchronized是JVM内置的,因此是内置特性。
- 释放锁的方式:
- Lock 必须要在finally中手动释放锁
- synchronized 会根据锁区域代码自动执行完毕,或者发生异常,JVM会自动释放锁
- 公平:
- Lock是可公平可不公平锁
- synchronized是不公平锁
ReentrantLock的使用:
基本使用
Lock lock = new ReentrantLock(); lock.lock(); try { //.... } finally { lock.unlock(); }
- lock: 调用后一直阻塞直到获得锁。
package com.amber; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestReentrantLock { Lock lock = new ReentrantLock(); public static void main(String[] args) { TestReentrantLock testReentrantLock = new TestReentrantLock(); new Thread(() -> { try { testReentrantLock.testConcurrency(Thread.currentThread()); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程1").start(); new Thread(() -> { try { testReentrantLock.testConcurrency(Thread.currentThread()); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程2").start(); } private void testConcurrency(Thread thread) throws InterruptedException { //获取锁成功返回true,如果获取失败,等待2S,规定时间内还是没有获得锁,那么就返回false if (lock.tryLock( 2000, TimeUnit.MICROSECONDS)) { try { System.out.println(thread.getName() + " : " + "获取锁"); Thread.sleep(3000); } finally { System.out.println(thread.getName() + "释放锁"); lock.unlock(); //一定记得要释放锁 } } else { System.out.println(Thread.currentThread().getName() + " : " + "等待了,没有获取锁"); } } }
- tryLock:拿到锁返回true,否则false;带有时间限制的tryLock(long time, TimeUnit timeUnit),拿不到锁,就等待一段时间,超时返回false
- lockInterruptibly :调用后如果没有获取到锁会一直阻塞,阻塞过程中会接受中断信号。
lockInterruptibly有点难以理解,假设A线程想去获取锁,但是锁被B线程持有,那么A就会发生堵塞。
A堵塞的时候,可以有以下两种方法发生状态改变:
- A获取锁资源
- A被其他线程中断:
- 这里只得被其他线程中断的意思是,C线程调用A线程的interrupt()。那么此时A线程就会被唤醒,处理中断信号。
lockInterruptibly是被中断,就由阻塞状态被唤醒去处理中断信号。
在JAVA并发编程实战这本书中还提到了ReentrantLock的一个重要用法,那就是轮询锁。下面是书中的源代码:
1 public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { 2 long fixedDelay = 1; 3 long randMod = 2; 4 long stopTime = System.nanoTime() + unit.toNanos(timeout); 5 while (true) { 6 if (fromAcct.lock.tryLock()) { 7 try { 8 if (toAcct.lock.tryLock()) { //如果不能同事获得两个锁,那么线程就会释放已经获得的锁。这样可以很有效的解决死锁问题。 9 try { 10 if (fromAcct.getBalance().compareTo(amount) < 0) 11 throw new InsufficientFundsException(); 12 else{ 13 fromAcct.debit(amount); 14 toAcct.credit(amount); 15 returntrue; 16 } 17 } finally { 18 toAcct.lock.unlock(); 19 } 20 } 21 } finally { 22 fromAcct.lock.unlock(); 23 } 24 } 25 if (System.nanoTime() < stopTime) 26 returnfalse; 27 NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); 28 } 29 }
程序发生死锁的时候,往往只能通过重新启动程序解决。而有时候因为获取锁的时序不一致,很容易发生死锁。根据上述代码第6行和第8行,假设此时我们使用的synochronized内置锁,A线程从cc账号转账到dd账号,B线程从dd账号转账到cc账号,就很容易发生死锁。但是使用tryLock()却可以避免锁顺序造成死锁的问题,
如果线程A、B不能同时获取cc和dd对象的锁,那么就会放弃自己已经获得的锁。