前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程之ReentrantLock

并发编程之ReentrantLock

作者头像
lyb-geek
发布2018-03-27 15:23:10
7700
发布2018-03-27 15:23:10
举报
文章被收录于专栏:Linyb极客之路Linyb极客之路

一、简述

ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”。

ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现锁的关键)来实现锁的获取与释放。其可以完全替代 synchronized 关键字。JDK 5.0 早期版本,其性能远好于 synchronized,但 JDK 6.0 开始,JDK 对 synchronized 做了大量的优化,使得两者差距并不大。

“独占”,就是在同一时刻只能有一个线程获取到锁,而其它获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

“可重入”,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

该锁还支持获取锁时的公平和非公平性选择。“公平”是指“不同的线程获取锁的机制是公平的”,而“不公平”是指“不同的线程获取锁的机制是非公平的”。

二、简单实例

代码语言:javascript
复制
public class ReenterLock implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;
    public void run() {
        for (int j = 0;j<100000;j++) {
            lock.lock();
//            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
//                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReenterLock reenterLock = new ReenterLock();
        Thread t1 = new Thread(reenterLock);
        Thread t2 = new Thread(reenterLock);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

与 synchronized 相比,重入锁有着显示的操作过程,何时加锁,何时释放,都在程序员的控制中。

为什么称作是“重入”?这是因为这种锁是可以反复进入的。将上面代码中注释部分去除注释,也就是连续两次获得同一把锁,两次释放同一把锁,这是允许的。

注意,获得锁次数与释放锁次数要相同,如果释放锁次数多了,会抛出 java.lang.IllegalMonitorStateException 异常;如果释放次数少了,相当于线程还持有这个锁,其他线程就无法进入临界区。

除了简单的加锁、解锁操作,重入锁还提供了一些更高级的功能,下面结合实例进行简单介绍:

  1. 中断响应(lockInterruptibly)

对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,获得这把锁继续执行,或者线程就保持等待。而使用重入锁,提供了另一种可能,这就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的需求。

下面的例子中,产生了死锁,但得益于锁中断,最终解决了这个死锁:

代码语言:javascript
复制
public class IntLock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /**
     * 控制加锁顺序,产生死锁
     */
    public IntLock(int lock) {
        this.lock = lock;
    }
    public void run() {
        try {
            if (lock == 1) {
                lock1.lockInterruptibly(); // 如果当前线程未被 中断,则获取锁。
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            } else {
                lock2.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 查询当前线程是否保持此锁。
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ",退出。");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(1);
        IntLock intLock2 = new IntLock(2);
        Thread thread1 = new Thread(intLock1, "线程1");
        Thread thread2 = new Thread(intLock2, "线程2");
        thread1.start();
        thread2.start();
        Thread.sleep(1000);//56行
        thread2.interrupt(); // 中断线程2//57行
    }
}

上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便形成 thread1 和 thread2 之间的相互等待。

代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已获得的 lock2。这个操作导致 thread1 顺利获得 lock2,从而继续执行下去。

执行代码,输出如下:

2.锁申请等待限时(tryLock)

除了等待外部通知(中断操作 interrupt )之外,限时等待也可以做到避免死锁。

通常,无法判断为什么一个线程迟迟拿不到锁。也许是因为产生了死锁,也许是产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。可以使用 tryLock() 方法进行一次限时的等待。

代码语言:javascript
复制
public class TimeLock implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6 * 1000);
            }else {
                System.out.println(Thread.currentThread().getName()+" get Lock Failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 查询当前线程是否保持此锁。
            if (lock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName()+" release lock");
                lock.unlock();
            }
        }
    }
    /**
     * 在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法再5秒的等待时间内获得锁,因此请求锁会失败。
     */
    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        Thread t1 = new Thread(timeLock, "线程1");
        Thread t2 = new Thread(timeLock, "线程2");
        t1.start();
        t2.start();
    }
}

上述例子中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此,请求锁失败。

ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁成功,立即返回 true。否则,申请失败,立即返回 false,当前线程不会进行等待。这种模式不会引起线程等待,因此也不会产生死锁。

3.公平锁

默认情况下,锁的申请都是非公平的。也就是说,如果线程 1 与线程 2,都申请获得锁 A,那么谁获得锁不是一定的,是由系统在等待队列中随机挑选的。这就好比,买票的人不排队,售票姐姐只能随机挑一个人卖给他,这显然是不公平的。而公平锁,它会按照时间的先后顺序,保证先到先得。公平锁的特点是:不会产生饥饿现象。

重入锁允许对其公平性进行设置。构造函数如下:

public ReentrantLock(boolean fair)

下面举例来说明,公平锁与非公平锁的不同:

代码语言:javascript
复制
public class FairLock implements Runnable{
    public static ReentrantLock fairLock = new ReentrantLock(true);
    public void run() {
        while (true) {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName()+",获得锁!");
            }finally {
                fairLock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        FairLock fairLock = new FairLock();
        Thread t1 = new Thread(fairLock, "线程1");
        Thread t2 = new Thread(fairLock, "线程2");
        t1.start();t2.start();
    }
}

修改重入锁是否公平,观察输出结果,如果公平,输出结果始终为两个线程交替的获得锁,如果是非公平,输出结果为一个线程占用锁很长时间,然后才会释放锁,另个线程才能执行。

三、ReentrantLock的几个重要方法

lock():获得锁,如果锁被占用,进入等待。

lockInterruptibly():获得锁,但优先响应中断。

tryLock():尝试获得锁,如果成功,立即放回 true,反之失败返回 false。该方法不会进行等待,立即返回。

tryLock(long time, TimeUnit unit):在给定的时间内尝试获得锁。

unLock():释放锁。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-02-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linyb极客之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档