ReentrantLock重入锁

一、ReentrantLock重入锁

1、ReentrantLock重入锁简介

ReentrantLock可以完全替代synchronized关键字。在JDK5.0的早期版本中,ReentrantLock的性能远远好于synchronized,但是凶JDK6.0开始synchronized上做了大量优化,使得两者性能差距并不大。

重入锁使用java.until.concurrent.locks.ReentrantLock类来实现。ReenterLock.java展示了简单的.ReentrantLock使用案例。

public class ReenterLock implements Runnable
{
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run()
    {
        for (int j = 0; j < 10000000; ++j)
        {
            lock.lock();    //对临界区加锁
            try
            {
                ++i;
            }
            finally
            {
                lock.unlock(); //退出临界区时必须释放锁
            }
        }
    }

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

从上面的代码可以看到,与synchronized相比,ReentrantLock有着显式的操作过程。开发人员必须手动指定何时加锁,何时释放锁。因此ReentrantLock对逻辑控制的灵活性远好于synchronized。但是必须注意,在退出临界区时,一定要释放锁。否则其他线程就没机会再访问临界区了。

ReentrantLock之所以叫重入锁,是因为这种锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程。
如下面代码展示:

            lock.lock();    //对临界区加锁
            lock.lock();
            try
            {
                ++i;
            }
            finally
            {
                lock.unlock(); //退出临界区时必须释放锁
                lock.unlock();
            }

在这种情况下,一个线程连续两次获得通一把锁,这是允许的,如果不允许的话,那么第二个线程在第2次获得锁时,将会和自己产生死锁。但是值得注意的是,如果一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数,如果释放锁的次数多,那么就会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放的锁次数少了,那么相当于线程还持有这个锁,其他线程也无法进入临界区。

2、中断响应

当使用synchronized来对临界区加锁,如果一个线程在等待锁,那么结果只有两种情况,要么它获得锁继续执行,要么它就保持等待。但是如果使用重入锁,就会提供另一种选择,那就是线程可以被中断。也就是说在等待锁的过程中,程序可以根据需要取消对锁的请求

下面的代码IntLock.java会产生一个死锁,但是得益于锁中断,我们可以很轻易的解决这个死锁。

public class IntLock implements Runnable
{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    /**
     * 控制加锁顺序,方便构造死锁
     * @param lock
     */
    public IntLock(int lock)
    {
        this.lock = lock;
    }

    @Override
    public void run()
    {
        try
        {
            if (lock == 1)
            {
                lock1.lockInterruptibly();
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {}
                lock2.lockInterruptibly();
            }
            else
            {
                lock2.lockInterruptibly();
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {}
                lock1.lockInterruptibly();
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (lock1.isHeldByCurrentThread())
            {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread())
            {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getId() +":线程退出");
        }
    }


    public static void main(String[] args) throws InterruptedException
    {

        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);

        //中断其中一个线程
        t2.interrupt();
    }
}

线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再占用lock1。因此很容易形成t1和t2之间的相互等待。在这里对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断

当代码执行到Thread.sleep(1000)时,主线程处于休眠,此时,这两个线程处于死锁的状态,当执行到t2.interrupt()时,由于t2被中断,故t2会放弃对lock1的申请,同时释放已获得的lock2,这个操作导致t1线程可以顺利得到lock2而继续执行下去。

执行上述代码,可以看到以下输出:


image.png

可以看到,中断后,两个线程双双退出。但真正完成工作的只有t1,而t2线程则放弃其任务直接退出,释放资源。

3、锁申请等待限时

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

下面代码TimeLock.java展示了限时等待锁的使用

public class TimeLock implements Runnable
{
    public static ReentrantLock lock = new ReentrantLock();


    @Override
    public void run()
    {
        try
        {
            if (lock.tryLock(5, TimeUnit.SECONDS))
            {
                Thread.sleep(6000);
            }
            else
            {
                System.out.println("get lock failed!");
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (lock.isHeldByCurrentThread())
            {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args)
    {
        TimeLock tl = new TimeLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);

        t1.start();
        t2.start();
    }
}

在这里,tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长是5,表示线程在这个锁请求中,最多等待5秒。如果超过五秒还没有得到锁,就会返回false。如果成功获得锁,则返回true。

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

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

4、公平锁

大多数情况下,锁的申请都是非公平的。举个例子,线程1首先请求了锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可以获得锁还是线程2可以获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:

public ReentrantLock(boolean fair)

当参数fair为true时,表示锁时公平的。但是实现公平锁必须要牺牲一定的性能,因为需要维护一个有序的队列,因此默认情况下,锁是非公平的。如果没有特别的要求,也不需要使用公平锁。下面的代码可以很好的突出公平锁的特点:

public class FairLock implements Runnable
{
    //当参数fair为true时,表示锁是公平的。默认为false,非公平
    public static ReentrantLock fairLock = new ReentrantLock(true);
    @Override
    public void run()
    {
        while (true)
        {
            try
            {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + " 获得锁");
            }
            finally
            {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args)
    {
        FairLock r1 = new FairLock();
        Thread t1 = new Thread(r1, "Thread_t1");
        Thread t2 = new Thread(r1, "Thread_t2");
        t1.start();
        t2.start();
    }
}

执行代码,会看到如下输出结果:


因为代码会产生大量输出,这里只截取部分进行说明。在这个输出中,很明显可以看到,两个线程基本上是交替获得锁的,几乎不会发生一个线程连续多次获得锁的可能,从而公平性也得到了保证。但是如果使用不公平锁,那么情况就会不一样,部分输出如下:


可以看到,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。

5、总结

(1)ReentrantLock 几个重要方法
  • lock():获得锁,如果已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。
(2)重入锁实现中包含的三要素
  • 原子状态:原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有。
  • 等待队列:所有没有请求到锁的线程,会进入等待队列进行等待,待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
  • 阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程会被挂起。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容