JUC之重入锁

重入锁可以完全替代synchronized关键字。在JDK 6.0以前,重入锁的性能远远好于synchronized,但是在JDK 6.0以后,JDK在synchronized上做了大量的优化,两者的性能差距并不大。

重入锁

下面是一个重入锁简单的例子:

package dgb.test.concurrent;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Dongguabai
 * @date 2018/9/3 11:10
 */
public class ReentrantLockTest implements Runnable {

    private static final ReentrantLock lock = new ReentrantLock();
    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest test = new ReentrantLockTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            lock.lock();
            try {
                i++;
            } finally {
                lock.unlock();
            }
        }
    }
}

可以看出与synchronized相比,重入锁有着显示的操作过程,必须手动指定何时加锁何时释放锁,非常灵活,要注意的是,在退出临界区时一定要记得释放锁。

重入锁中重入的意思就是同一个线程可以连续多次获取同一把锁,因为如果不允许的话,同一个线程在第二次获得锁的时候会和自己产生死锁,程序会卡死在第二次获取锁的过程中。如果同一个线程多次获得重入锁,那么在释放锁的时候,也必须释放相同的次数,如果释放的次数多,会产生IllegalMonitorStateException异常;如果释放的次数少了,那么相当于这个线程还持有这个锁。

几个重要的API

  • lock():获得锁,如果锁已经被占用,则等待
  • lockInterruptibly():如果当前线程未被中断,获取锁(在等待锁的过程中可以响应中断)
    • lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常
  • tryLock():尝试获得锁,如果成功,返回true,失败则返回false。该方法不等待,会立即返回(可以和for(;;)结合一起用)
  • tryLock(long timeout,TimeUnit unit):尝试获得锁,如果成功,返回true,失败则返回false。会等待给定时间
  • unlock():释放锁
  • getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
  • getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
  • hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  • hasQueuedThreads():是否有线程等待此锁
  • isFair():该锁是否公平锁
  • isHeldByCurrentThread():当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
  • isLock():此锁是否有任意线程占用
  • getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
  • hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法

重入锁的实现主要集中在Java层面。重入锁主要包含三个要素:

  1. 原子状态。原子状态时使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有;
  2. 等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作;
  3. 是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

公平锁

重入锁主要是使用java.util.concurrent.locks.ReentrantLock类。有两个构造方法:

Lock lock=new ReentrantLock(true);//公平锁
Lock lock=new ReentrantLock(false);//非公平锁

公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock的线程不一定先获得锁。在大多数情况下,锁的申请都是非公平的,如果我们使用synchronized关键字进行锁的控制,那么产生的锁就是非公平锁。

但是实现公平锁系统就必须要维护一个有序队列,因此公平锁的成本相对比较高,性能也相对低下。

《JUC之重入锁》

《JUC之重入锁》

下面是一个公平锁简单的例子:

package dgb.test.concurrent;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Dongguabai
 * @date 2018/9/3 11:46
 */
public class ReentrantLockTest2 implements Runnable {

    private static final ReentrantLock lock = new ReentrantLock(true);


    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                System.out.println("线程【" + Thread.currentThread().getName() + "】获得锁-----");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockTest2 reentrantLockTest2 = new ReentrantLockTest2();
        new Thread(reentrantLockTest2,"thread-1").start();
        new Thread(reentrantLockTest2,"thread-2").start();
    }
}

输出结果:

《JUC之重入锁》

可以看出两个线程是交替获得锁的,几乎不会发生同一个线程连续多次获得锁的可能。从而公平性得到了保证。

中断响应

对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它能获得这把锁继续执行,要么就保持等待。重入锁提供了一种中断的功能,就是在等待锁的过程中,程序可以根据需要取消对锁的请求。比如你跟朋友越好出去玩,等了一会朋友还没来,这时候接了个电话让你去做别的事情,这时候你就可以不用继续等待了。中断提供了一套类似的机制,如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。这种情况对于处理死锁是有一定的帮助的。

先看下面一段代码:

package dgb.test.concurrent;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Dongguabai
 * @date 2018/9/3 17:33
 */
public class ReentrantLockTest4 implements Runnable {

    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();

    private int lock;

    public ReentrantLockTest4(int lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            if (lock == 1) {
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock1");
                lock1.lockInterruptibly();
                TimeUnit.SECONDS.sleep(1);
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock2");
                lock2.lockInterruptibly();
            }else {
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock2");
                lock2.lockInterruptibly();
                TimeUnit.SECONDS.sleep(1);
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock1");
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("线程【"+Thread.currentThread().getName()+"】进入finally");
            if (lock1.isHeldByCurrentThread()){
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始释放锁lock1");
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()){
                System.out.println("线程【"+Thread.currentThread().getName()+"】开始释放锁lock2");
                lock2.unlock();
            }
            System.out.println("线程【"+Thread.currentThread().getName()+"】退出-------");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest4 test1 = new ReentrantLockTest4(1);
        ReentrantLockTest4 test2 = new ReentrantLockTest4(2);
        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test2);
        t1.start();
        t2.start();
    }
}

很明显,构成了死锁:

《JUC之重入锁》

这时候可以中断一个线程:

《JUC之重入锁》

输出结果:

《JUC之重入锁》

可以看到两个线程双双退出,只有一个线程完成了工作,另一个线程直接放弃任务直接退出了。

锁申请等待时间

避免死锁还有另一种方式,就是限时等待。比如你跟朋友出去玩,然后你等朋友等了一个小时朋友还没来,那就不等了。通常,我们无法判断一个线程为什么一直拿不到锁。可能是因为死锁,可能是因为饥饿。如果给定一个时间限制,如果超过了这个时间就让线程自动放弃,这样是有实际意义的。我们可以使用tryLock()方法。

下面是一个简单的示例:

package dgb.test.concurrent;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Dongguabai
 * @date 2018/9/3 17:33
 */
public class ReentrantLockTest5 implements Runnable {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest5 reentrantLockTest5 = new ReentrantLockTest5();
        Thread t1 = new Thread(reentrantLockTest5,"thread1");
        Thread t2 = new Thread(reentrantLockTest5,"thread2");
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                //持有6秒
                TimeUnit.SECONDS.sleep(6);
            } else {
                System.out.println("线程【" + Thread.currentThread().getName() + "】加锁失败!!!!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("线程【" + Thread.currentThread().getName() + "】进入finally");
            if (lock.isHeldByCurrentThread()) {
                System.out.println("线程【" + Thread.currentThread().getName() + "】开始释放锁lock1");
            }
        }
    }

}

Condition条件

Condition条件和Object#wait()、Object#notify()的作用大致相同,但是Object#wait()和Object#notify()需要与synchronized关键字一起合作使用,而Condition是与重入锁相关联的。

ReentrantLock实现了Lock接口,在Lock接口中有这样一个方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition实例我们可以让线程在合适的时候等待,或者在某一个特定的时刻得到通知,继续执行。

Condition接口的基本方法如下:

  • void await() throws InterruptedException

      使当前线程进入等待状态,同时释放当前锁。当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待,这和Object#wait()很像;

  • void awaitUninterruptibly()

当前线程进入等待状态,直到被通知,对中断不做响应;

  • long awaitNanos(long nanosTimeout) throws InterruptedException
  • boolean await(long time, TimeUnit unit) throws InterruptedException
  • boolean awaitUntil(Date deadline) throws InterruptedException

当前线程进入等待状态直到将来的指定时间被通知,如果没有到指定时间被通知返回true,否则,到达指定时间,返回false;

  • void signal()

唤醒一个等待在Condition上的线程;

  • void signalAll()

唤醒等待在Condition上所有的线程;

和Object#wait()、Object#notify()类似,当线程使用Condition#await()的时候,要求线程持有相关的重入锁,当线程调用Condition#await()后,这个线程需要释放这把锁

下面简单演示一下Condition的用法:

package dgb.test.concurrent;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Dongguabai
 * @date 2018/9/3 16:07
 */
public class ReentrantLockTest3 implements Runnable {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    @Override
    public void run() {
        System.out.println("线程【" + Thread.currentThread().getName() + "】开始执行---------------");
        try {
            lock.lock();
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println("线程【" + Thread.currentThread().getName() + "】解锁---------------");
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("线程【" + Thread.currentThread().getName() + "】开始执行---------------");
        ReentrantLockTest3 reentrantLockTest3 = new ReentrantLockTest3();
        Thread thread = new Thread(reentrantLockTest3);
        thread.start();
        Thread.sleep(5000);
        System.out.println("线程【" + Thread.currentThread().getName() + "】发出通知");
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

执行流程分析如下:

《JUC之重入锁》

 

    原文作者:JUC
    原文地址: https://blog.csdn.net/Dongguabai/article/details/82345858
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞