Java 锁
JDK中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLocksynchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

两者的相同点

ReentrantLock是独占锁且可重入的

例子

  1. public class ReentrantLockTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. ReentrantLock lock = new ReentrantLock();
  4. for (int i = 1; i <= 3; i++) {
  5. lock.lock();
  6. }
  7. for(int i=1;i<=3;i++){
  8. try {
  9. } finally {
  10. lock.unlock();
  11. }
  12. }
  13. }
  14. }

上面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,可以得出以下ReentrantLocksynchronized的相同点。

  • ReentrantLocksynchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
  • ReentrantLocksynchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要确保保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

    两者的额外功能

    ReentrantLock可以实现公平锁

    公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

    1. ReentrantLock lock = new ReentrantLock(true);

    继续跟进看下源码

    1. /**
    2. * Creates an instance of {@code ReentrantLock} with the
    3. * given fairness policy.
    4. *
    5. * @param fair {@code true} if this lock should use a fair ordering policy
    6. */
    7. public ReentrantLock(boolean fair) {
    8. sync = fair ? new FairSync() : new NonfairSync();
    9. }

    可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。

    一个公平锁的例子

    1. public class ReentrantLockTest {
    2. static Lock lock = new ReentrantLock(true);
    3. public static void main(String[] args) throws InterruptedException {
    4. for(int i=0;i<5;i++){
    5. new Thread(new ThreadDemo(i)).start();
    6. }
    7. }
    8. static class ThreadDemo implements Runnable {
    9. Integer id;
    10. public ThreadDemo(Integer id) {
    11. this.id = id;
    12. }
    13. @Override
    14. public void run() {
    15. try {
    16. TimeUnit.MILLISECONDS.sleep(10);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. for(int i=0;i<2;i++){
    21. lock.lock();
    22. System.out.println("获得锁的线程:"+id);
    23. lock.unlock();
    24. }
    25. }
    26. }
    27. }

    公平锁结果

    重入锁 - 图1
    开启5个线程,让每个线程都获取释放锁两次。为了能更好的观察到结果,在每次获取锁前让线程休眠10毫秒。可以看到线程几乎是轮流的获取到了锁。如果改成非公平锁,再看下结果

    非公平锁结果

    重入锁 - 图2
    线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

    公平锁和非公平锁该如何选择?

    大部分情况下使用非公平锁,因为其性能比公平锁好很多。
    但是公平锁能够避免线程饥饿,某些情况下也很有用。

    ReentrantLock可响应中断

    当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

    响应中断的例子:

    1. public class ReentrantLockTest {
    2. static Lock lock1 = new ReentrantLock();
    3. static Lock lock2 = new ReentrantLock();
    4. public static void main(String[] args) throws InterruptedException {
    5. Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
    6. Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
    7. thread.start();
    8. thread1.start();
    9. thread.interrupt();//是第一个线程中断
    10. }
    11. static class ThreadDemo implements Runnable {
    12. Lock firstLock;
    13. Lock secondLock;
    14. public ThreadDemo(Lock firstLock, Lock secondLock) {
    15. this.firstLock = firstLock;
    16. this.secondLock = secondLock;
    17. }
    18. @Override
    19. public void run() {
    20. try {
    21. firstLock.lockInterruptibly();
    22. TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
    23. secondLock.lockInterruptibly();
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. } finally {
    27. firstLock.unlock();
    28. secondLock.unlock();
    29. System.out.println(Thread.currentThread().getName()+"正常结束!");
    30. }
    31. }
    32. }
    33. }

    结果:重入锁 - 图3
    构造死锁场景:
    创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。
    如果没有外界中断,该程序将处于死锁状态永远无法停止。通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

    获取锁时限时等待

    ReentrantLock还提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。可以使用该方法配合失败重试机制来更好的解决死锁问题。

    更好的解决死锁的例子:

    1. public class ReentrantLockTest {
    2. static Lock lock1 = new ReentrantLock();
    3. static Lock lock2 = new ReentrantLock();
    4. public static void main(String[] args) throws InterruptedException {
    5. Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
    6. Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
    7. thread.start();
    8. thread1.start();
    9. }
    10. static class ThreadDemo implements Runnable {
    11. Lock firstLock;
    12. Lock secondLock;
    13. public ThreadDemo(Lock firstLock, Lock secondLock) {
    14. this.firstLock = firstLock;
    15. this.secondLock = secondLock;
    16. }
    17. @Override
    18. public void run() {
    19. try {
    20. while(!lock1.tryLock()){
    21. TimeUnit.MILLISECONDS.sleep(10);
    22. }
    23. while(!lock2.tryLock()){
    24. lock1.unlock();
    25. TimeUnit.MILLISECONDS.sleep(10);
    26. }
    27. } catch (InterruptedException e) {
    28. e.printStackTrace();
    29. } finally {
    30. firstLock.unlock();
    31. secondLock.unlock();
    32. System.out.println(Thread.currentThread().getName()+"正常结束!");
    33. }
    34. }
    35. }
    36. }

    结果:重入锁 - 图4
    线程通过调用tryLock()方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。

    结合Condition实现等待通知机制

    使用synchronized结合Object上的waitnotify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

    Condition使用简介

    ConditionReentrantLock对象创建,并且可以同时创建多个 ```java static Condition notEmpty = lock.newCondition();

static Condition notFull = lock.newCondition();

  1. `Condition`接口在使用前必须先调用`ReentrantLock``lock()`方法获得锁。之后调用`Condition`接口的`await()`将释放锁,并且在该`Condition`上等待,直到有其他线程调用`Condition``signal()`方法唤醒线程。使用方式和`wait``notify`类似。
  2. <a name="HTgXj"></a>
  3. #### 一个使用`condition`的简单例子:
  4. ```java
  5. public class ConditionTest {
  6. static ReentrantLock lock = new ReentrantLock();
  7. static Condition condition = lock.newCondition();
  8. public static void main(String[] args) throws InterruptedException {
  9. lock.lock();
  10. new Thread(new SignalThread()).start();
  11. System.out.println("主线程等待通知");
  12. try {
  13. condition.await();
  14. } finally {
  15. lock.unlock();
  16. }
  17. System.out.println("主线程恢复运行");
  18. }
  19. static class SignalThread implements Runnable {
  20. @Override
  21. public void run() {
  22. lock.lock();
  23. try {
  24. condition.signal();
  25. System.out.println("子线程通知");
  26. } finally {
  27. lock.unlock();
  28. }
  29. }
  30. }
  31. }

运行结果:重入锁 - 图5

使用Condition实现简单的阻塞队列

阻塞队列是一种特殊的先进先出队列,它有以下几个特点

  1. 入队和出队线程安全
  2. 当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

    阻塞队列的简单实现:

    1. public class MyBlockingQueue<E> {
    2. int size;//阻塞队列最大容量
    3. ReentrantLock lock = new ReentrantLock();
    4. LinkedList<E> list=new LinkedList<>();//队列底层实现
    5. Condition notFull = lock.newCondition();//队列满时的等待条件
    6. Condition notEmpty = lock.newCondition();//队列空时的等待条件
    7. public MyBlockingQueue(int size) {
    8. this.size = size;
    9. }
    10. public void enqueue(E e) throws InterruptedException {
    11. lock.lock();
    12. try {
    13. while (list.size() ==size)//队列已满,在notFull条件上等待
    14. notFull.await();
    15. list.add(e);//入队:加入链表末尾
    16. System.out.println("入队:" +e);
    17. notEmpty.signal(); //通知在notEmpty条件上等待的线程
    18. } finally {
    19. lock.unlock();
    20. }
    21. }
    22. public E dequeue() throws InterruptedException {
    23. E e;
    24. lock.lock();
    25. try {
    26. while (list.size() == 0)//队列为空,在notEmpty条件上等待
    27. notEmpty.await();
    28. e = list.removeFirst();//出队:移除链表首元素
    29. System.out.println("出队:"+e);
    30. notFull.signal();//通知在notFull条件上等待的线程
    31. return e;
    32. } finally {
    33. lock.unlock();
    34. }
    35. }
    36. }

    测试代码 ```java public static void main(String[] args) throws InterruptedException {

    MyBlockingQueue queue = new MyBlockingQueue<>(2); for (int i = 0; i < 10; i++) {

    1. int data = i;
    2. new Thread(new Runnable() {
    3. @Override
    4. public void run() {
    5. try {
    6. queue.enqueue(data);
    7. } catch (InterruptedException e) {
    8. }
    9. }
    10. }).start();

    } for(int i=0;i<10;i++){

    1. new Thread(new Runnable() {
    2. @Override
    3. public void run() {
    4. try {
    5. Integer data = queue.dequeue();
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. }
    10. }).start();

    }

} ``` 运行结果:重入锁 - 图6

总结

ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。