Java的锁—彻底理解重入锁(ReentrantLock)

重入锁简单理解就是对同一个线程而言,它可以重复的获取锁。例如这个线程可以连续获取两次锁,但是释放锁的次数也一定要是两次。下面是一个简单例子:

public class ReenterLock {

    private static ReentrantLock lock = new ReentrantLock();

    private static int i = 0;

    // 循环1000000次
    private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    });

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        // 利用join,等thread1,thread2结束后,main线程才继续运行,并打印 i
        thread1.join();
        thread2.join();
        // 利用lock保护的 i,最终结果为 2000000,如果不加,则值肯定小于此数值
        System.out.println(i);
    }
}

从上面的代码可以看到,相比于synchronized,开发者必须手动指定锁的位置和什么时候释放锁,这样必然增加了灵活性。

线程中断响应

如果线程阻塞于synchronized,那么要么获取到锁,继续执行,要么一直等待。重入锁提供了另一种可能,就是中断线程。下面的例子是利用两个线程构建一个死锁,然后中断其中一个线程,使另一个线程获取锁的例子:

public class ReenterLockInterrupt {
    private static ReentrantLock lock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // 利用 lockInterruptibly 申请锁,这是可以进中断申请的申请锁操作
            lock.lockInterruptibly();
            // 睡眠20秒,在睡眠结束之前,main方法里要中断thread2的获取锁操作
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            String threadName = Thread.currentThread().getName();
            // 中断后抛出异常,最后要释放锁
            // 如果是线程1则释放锁,因为线程2就没拿到锁,所以不用释放
            if ("Thread-1".equals(threadName)) lock.unlock();
            System.out.println(threadName+" 停止");
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");
        thread1.start();

        // 让主线程停一下,让thread1获取锁后再启动thread2
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 这里什么也不做
        }

        thread2.start();
        thread2.interrupt();
    }
}

thread-1拿到锁之后,线程即持有锁并等待20秒,然后thread-2启动,并没有拿到锁,这时候中断thread-2线程,线程2退出。

有限时间的等待锁

顾名思义,简单理解就是在指定的时间内如果拿不到锁,则不再等待锁。当持有锁的线程出问题导致长时间持有锁的时候,你不可能让其他线程永远等待其释放锁。下面是一个例子:

public class ReenterTryLock {
    private static ReentrantLock reenterLock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // tryLock()方法会返回一个布尔值,获取锁成功则为true
            if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
                Thread.sleep(5000);
            } else {
                System.out.println(Thread.currentThread().getName() + "获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 最后,如果当前前程在持有锁,则释放锁
            if (reenterLock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "释放锁了");
                reenterLock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");

        thread1.start();
        thread2.start();
    }
}

这里使用tryLock()第一个获取锁的线程,会停止5秒。而获取锁的设置为3秒获取不到锁则放弃,所以第二个去尝试获取锁的线程是获取不到锁而被迫停止的。如果tryLock()方法不传入任何参数,那么获取锁的线程不会等待锁,则立即返回false。

公平锁与非公平锁

当一个线程释放锁时,其他等待的线程则有机会获取锁,如果是公平锁,则分先来后到的获取锁,如果是非公平锁则谁抢到锁算谁的,这就相当于排队买东西和不排队买东西是一个道理。Java的synchronized关键字就是非公平锁

那么重入锁ReentrantLock()是公平锁还是非公平锁?

重入锁ReentrantLock()是可以设置公平性的,可以参考其构造方法:

// 通过传入一个布尔值来设置公平锁,为true则是公平锁,false则为非公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

构建一个公平锁需要维护一个有序队列,如果实际需求用不到公平锁则不需要使用公平锁。下面用一个例子来演示公平锁与非公平锁的区别:

public class ReenterTryLockFair {
    // 分别设置公平锁和非公平锁,分析打印结果
    private static ReentrantLock lock = new ReentrantLock(true);

    private static Runnable runnable = () -> {
        while (true) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
            } finally {
                lock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread---1");
        Thread thread2 = new Thread(runnable, "thread---2");
        Thread thread3 = new Thread(runnable, "thread---3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

当设置为true即公平锁的时候,可以看到打印非常规律,截取一段儿打印结果:

thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁

可以看到,都是thread–1,thread–2,thread–3,无限循环下去,如果设置的为非公平锁,打印结果就混乱没有规律了:

thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---1 获取了锁

Condition

同jdk中的等待/通知机制类似,只不过Condition是用在重入锁这里的。有了Condition,线程就可以在合适的时间等待,在合适的时间继续执行。

Condition接口包含以下方法:

// 让当前线程等待,并释放锁
void await() throws InterruptedException;
// 和await类似,但在等待过程中不会相应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒等待中的线程
void signal();
// 唤醒等待中的所有线程
void signalAll();

下面是一个简单示例:

public class ReenterLockCondition {
    private static ReentrantLock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    private static Runnable runnable = () -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "进入等待。。");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable, "thread--1");
        thread.start();

        Thread.sleep(2000);

        lock.lock();
        condition.signal();
        System.out.println("主线程发出信号");
        lock.unlock();
    }
}

thread–1启动,拿到锁,然后进入等待并且释放锁,2秒后,主线程拿到锁,然后发出信号并释放锁,最后,thread–1继续执行。下面是打印结果:

thread--1进入等待。。
主线程发出信号
thread--1继续执行
    原文作者:java锁
    原文地址: https://blog.csdn.net/i_am_kop/article/details/80958856
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞