重入锁 ReentrantLock

本文详细介绍了Java的重入锁ReentrantLock,包括它可以替代synchronized的使用,重入特性,中断响应能力,锁申请等待限时功能,以及公平锁与非公平锁的概念。通过实例展示了ReentrantLock如何提升多线程控制的灵活性和处理死锁的能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.synchronized的功能扩展:重入锁

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

    重入锁使用java.util.concurrent.locks.ReentrantLock类来实现,我们先看一段简单的重入锁的使用案例:  

 /**

    * @ClassName ReenterLock

    * @Description 重入锁简单实现方式

    * @Author JinDuoWang

    * @Email wangjinduoliuxi@163.com

    * @Date 9:30 2019/1/18

    * @Version 1.0

    **/

    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 {

            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异常,

    // 反之,如果你加锁两次,但是你只释放了一次,那么后面的线程就永远无法进入临界区。
    lock.lock();

    lock.lock();

    try {

        i++;

    } finally {

        lock.unlock();

        lock.unlock();

    }

2.中断响应

    对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得了这个锁,要么就保持等待。而是用重入锁的话还会有另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,可根据需求来取消对锁的请求。

    下面的代码产了一个死锁,但得益于锁中断,我们可以很轻松的解决这个死锁。  

 /**

    * @ClassName IntLock

    * @Description 死锁模拟

    * @Author JinDuoWang

    * @Email wangjinduoliuxi@163.com

    * @Date 9:56 2019/1/18

    * @Version 1.0

    **/

    public class IntLock implements Runnable {



        public static ReentrantLock reentrantLock1 = new ReentrantLock();



        public static ReentrantLock reentrantLock2 = new ReentrantLock();



        int lock;



        public IntLock(int lock) {

            this.lock = lock;

        }



        @Override

        public void run() {

            try {

                if (lock ==1) {

                    reentrantLock1.lockInterruptibly();

                    try {

                        Thread.sleep(500);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                    reentrantLock2.lockInterruptibly();;

                } else {

                    reentrantLock2.lockInterruptibly();

                    try {

                        Thread.sleep(500);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                    reentrantLock1.lockInterruptibly();

                }

            } catch (InterruptedException e) {

                e.printStackTrace();

            } finally {

                if (reentrantLock1.isHeldByCurrentThread()) {

                    reentrantLock1.unlock();

                }

                if (reentrantLock2.isHeldByCurrentThread()) {

                    reentrantLock2.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);

        }

    }

    输出内容如下:

java.lang.InterruptedException

    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)

    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)

    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)

    at com.wjd.reenterLock.IntLock.run(IntLock.java:43)

    at java.lang.Thread.run(Thread.java:748)

11线程退出

12线程退出

    可以看出只有t1真正的完成任务后退出,t2则直接退出,释放资源。

3.锁申请等待限时

    除了等待外部通知以外,要避免死锁还有另一重方法,那就是限时等待tryLock(),下面我们来看一下tryLock()简单的实现方式:

    /**

    * @ClassName TimeLock

    * @Description 等待现实

    * @Author JinDuoWang

    * @Email wangjinduoliuxi@163.com

    * @Date 10:15 2019/1/18

    * @Version 1.0

    **/

    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("获取锁失败;");

                }

            } catch (InterruptedException e) {

                e.printStackTrace();

            } finally {

                lock.isHeldByCurrentThread();

                lock.unlock();

            }

        }



        public static void main(String[] args) {

            TimeLock timeLock = new TimeLock();

            Thread t1 = new Thread(timeLock);

            Thread t2 = new Thread(timeLock);

            t1.start();

            t2.start();

        }



    }

    我们可以看到tryLock()有两个参数,一个代表时长,一个代表计时单位,在这里我们设置了等待5秒,如果超过5秒还没有得到锁,就会返回false。如果成功获得锁就返回true。

    在本例中我们让线程休眠了6秒,让tryLock()获取不到锁,我们来看一下输出结果:

获取锁失败;

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException

    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)

    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)

    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)

    at com.wjd.reenterLock.TimeLock.run(TimeLock.java:30)

    at java.lang.Thread.run(Thread.java:748)

    有没有感觉到tryLock用起来很爽。

    tryLock我们就说到这里。

4.公平锁

    在大多数的情况下锁的申请都是非公平的,也就是说线程1请求了锁A,紧接着线程2也请求了锁A,那么当锁A可用的时候,是线程1能抢到锁A还是线程2呢?这是不一定的,系统会随机挑选一个,因此不能保证其公平性。如果我们使用的是synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们设置公平性,他有一个如下的构造函数:

public ReentrantLock(boolean fair)

    当fair等于true的时候,表示锁是公平的。公平锁固然是好,能保证每个线程都能够竞争到锁,但是要实现公平锁,系统必然要维护一个有序队列,因此公平锁的实现成本比较高,性能也非常低下,因此默认的情况下,都会使用非公平的,如果没有特殊的需求,也不需要使用公平锁。公平锁和非公平锁的线程调度也是不一样的,下面我们来看一下公平锁:

    /**

    * @ClassName FairLock

    * @Description 公平锁

    * @Author JinDuoWang

    * @Email wangjinduoliuxi@163.com

    * @Date 15:01 2019/1/18

    * @Version 1.0

    **/

    public class FairLock implements Runnable {

        

        // 将fair设置为true,则为公平锁

        public static ReentrantLock reentrantLock = new ReentrantLock(true);



        @Override

        public void run() {

            while (true) {

                try {

                    reentrantLock.lock();

                    System.out.println(Thread.currentThread().getName() + ":获得了锁");

                } finally {

                    reentrantLock.unlock();

                }

            }

        }



        public static void main(String[] args) {

            FairLock fairLock = new FairLock();

            Thread t1 = new Thread(fairLock, "THREAD-O1");

            Thread t2 = new Thread(fairLock, "THREAD-O2");

            t1.start();

            t2.start();

        }



    }

       在代码中,我们把fair设置为true,则指定锁为公平锁。接着t1和t2分别请求这把锁,并且在得到锁之后,进行一个输出,表示自己获得了锁,在公平锁的情况下,得到的输出通常是这样的:

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    代码会产生大量的输出,这里为了展示我只是截取了一部分拿来进行说明。在这个输出中明显可以看到,两个线程交替获得锁,你一次我一次非常和谐,几乎不会发生一个线程多次获得锁的可能,从而公平锁也得到了保证,我们再来看一下非公平锁,首先我们把代码修改一下,把fair设置为false或者直接不传(默认就是非公平的),修改完以后我们在来看一下输出的结果:

    前面还有很大一片THREAD-01的输出

    THREAD-O1:获得了锁

    THREAD-O1:获得了锁

    THREAD-O1:获得了锁

    THREAD-O1:获得了锁

    THREAD-O1:获得了锁

    THREAD-O1:获得了锁

    THREAD-O2:获得了锁

    THREAD-O2:获得了锁

    THREAD-O2:获得了锁

    THREAD-O2:获得了锁

    THREAD-O2:获得了锁

    后面还有很大一片THREAD-02的输出

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

    对上面的ReentrantLock的几个重要的方法整理如下。

方法名

介绍

lock()

获取锁,如果锁已经被占用,则等待。

lockInterruptibly()

获得锁,但是优先相应中断。

tryLock()

尝试获得锁,如果成功返回true,如果失败返回false。该方法不等待,立即返回。

tryLock(long time, TimeUnit unit)

在给定的时间内,尝试获得锁。

unlock()

释放锁。

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

    第一,是原子状态。原子状态使用CAS操作(会在之后的博文中讲解)来存储当前锁的状态,判断是否已被别的锁持有。

    第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就会从等待的队列中唤醒一个线程,继续工作。

    第三,是阻塞原语park()和unpark(),用来挂起和回复线程。没有获得锁的线程将会被挂起。

    Ps:有关park()和unpark()的详细介绍会在后面介绍。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值