Java多线程-09 (Lock锁之ReentrantLock可重入锁)

                                                        ReentrantLock 

                                                                                             个人博客:www.xiaobeigua.icu 


        synchronized被称为内部锁,在 JDK5 中增加了Lock 锁接口,lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。这也是多线程编程中一种保护线程安全的重要机制, 它也被称为外部所,此接口有多个实现类,下面我们就来学习下Lock接口的一些实现类

                                                                               

1.1 锁的可重入性

        锁的可重入是指,当一个线程获得一个对象锁后,再次请求该对象 锁时是可以获得该对象的锁的.

/**
 * 作者:小北呱
 */
public class text6 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                m1();
            }
        }).start();
    }

    //创建m1调用m2
    static synchronized void m1(){
        System.out.println(Thread.currentThread().getName()+"获得锁1");
        //调用m2
        m2();
    }

    static  synchronized void m2(){
        System.out.println(Thread.currentThread().getName()+"获得锁2");
    }
}

结果:


1.2 ReentrantLock

ReentrantLock 锁是Lock接口实现类之一,称为可重入锁, 它功能比 synchronized多

 1.2.1ReentranLock中实现Lock接口的方法

lock​()

获得锁。

unlock​()

释放锁。

tryLock​()

只有在调用时才可以获得锁。

tryLock​(long time, TimeUnit unit)

如果在给定的等待时间内没有锁定,并且当前线程尚未被 interrupted获取锁定。

lockInterruptibly​()

获取锁定,除非当前线程是 interrupted

newCondition​()

返回一个新Condition绑定到该实例Lock实例。

1.2.2 ReentrantLock 的基本使用

        调用 lock()方法获得锁, 调用 unlock()释放锁

代码

public class text4 {
    //创建可重入锁
    Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        text4 t4=new text4();
        //创建线程执行t4.toLock()方法
        Runnable runnable =new Runnable() {
            @Override
            public void run() {
                t4.toLock();
            }
        };
        //开启3个线程打印0-2
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();

    }

    //创建方法打印 0到2
    public void  toLock(){
        try {
            lock.lock();
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+"---"+i);
            }
        } finally {
            lock.unlock();
        }

    }
}

结果:

 1.2.3 ReentrantLock 锁的可重入性

public class text4 {
    //创建可重入锁
    Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        text4 t4=new text4();
        //创建线程执行1内部类方法
        Runnable runnable =new Runnable() {
            @Override
            public void run() {
                t4.toLock();
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();



    }

    //创建方法获得两个锁
    public void  toLock(){
        System.out.println(Thread.currentThread().getName()+"进入toLock方法");
        try {
            //测试重入性 获得两次锁
            lock.lock();
            lock.lock();
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+"获得2个锁");
            }
        } finally {
            System.out.println(Thread.currentThread().getName()+"退出2个锁");
            lock.unlock();
            lock.unlock();
        }

    }
}

结果:

 注意 进入多少个锁就要释放多少个锁,不然就会阻塞别的线程引起死锁

 比如:将上述代码中的两个 lock.unlock()改为一个

                 

则:线程 Thread--1无法获得两个锁,就会导致它一直处于等待状态

                

 1.2.4 lockInterruptibly()方法

        lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁, 如果当前线程被中断则出现异常。

        对于内部锁 synchronized 来说,如果一个线程在等待锁,只有两个结果:要么该线程获得锁继续执行;要么就保持等待. 对于 ReentrantLock 可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据需要取消对锁的请求,这点就比 synchronized要舒服的多。

例子 :创建2个线程来使用 用lockInterruptibly()锁主的方法

public class test5 {
    public static Lock lock=new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test5 t5=new test5();
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                try {
                    t5.toLock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t1=new Thread(runnable);
        Thread t2=new Thread(runnable);
        t1.start();
        Thread.sleep(1000);
        t2.start();
        t2.interrupt();

    }

    public void toLock() throws InterruptedException {
        //创建一个循环模拟耗时操作 例:创建int 类型的最大数次的String 对象
        lock.lockInterruptibly();
        System.out.println(Thread.currentThread().getName()+"开始创建String对象");
        for (int i=0;i<Integer.MAX_VALUE;i++){
            new String();
        }
        lock.unlock();
        System.out.println(Thread.currentThread().getName()+"创建对象结束");
    }
}

结果:

        可以发现代码中,我们不干预线程0执行,所以它正常执行完毕输出

        我们再main方法中调用了  t2.interrupt().把干开始执行的线程1给中断了,因为我们用的是 lock.lockInterruptibly()方法,所以抛出了异常。

注意:我们如果把lock.lockInterruptibly()改为 lock.lock()

结果:

         发现线程1调用了  t2.interrupt(),它还是完美执行,这是因为 使用 lock()方法锁就和使用synchronized内部锁是类似的,线程只有两种结果 要么获得锁执行, 要么等待,在等待中,是无法用interrupt()进行中断的。

 1.2.5 tryLock()方法

         tryLock(long time, TimeUnit unit) 的作用在给定等待时长内锁没有被另外的线程持有,并且当前线程也没有被中断,则获得该锁.通过该方法可以实现锁对象的限时等待。

         tryLock()tryLock(long time, TimeUnit unit) 都表示的是用来尝试获取锁:成功获取则返回true;获取失败则返回false。

        使用tryLock()tryLock(long time, TimeUnit unit)可以解决死锁问题,这是synchronized内部锁不具备的,也是Lock()的强大之处

        就是你去上厕所,里面有人,你尝试等几分钟,时间到了,如果人出来了,你也没有因为别的事离开,那就到你上厕所了,如果时间过了,人还没出来,那你就放弃等待。

                                           

例子:用tryLock(long time, TimeUnit unit)设置两个线程去争夺一个锁:

public class Test8 {
    //定义可重入锁
    private Lock lock=new ReentrantLock();

    public static void main(String[] args) {

        Test8 t8=new Test8();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t8.doThing();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                t8.doThing();
            }
        }).start();

    }

    //定义线程要做的任务
    public void doThing() {
        try {
            System.out.println(Thread.currentThread().getName()+"尝试获得锁....");
            //如果锁被别的线程占用,尝试等待3秒 ,这段时间内获得锁,就继续执行,没有就放弃
            if (lock.tryLock(3, TimeUnit.SECONDS)){//返回值为布尔值 true 或 false
                System.out.println("成功");
                System.out.println(Thread.currentThread().getName()+"获得锁,正在执行任务...");
                Thread.sleep(4000);//模拟执行任务花了4秒
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

结果: 

   结论:

         线程0和线程1都在尝试获得锁,线程1先获得锁,进入执行任务需要4秒,而线程0使用tryLock(),方法只尝试等待3秒 ,所以时间到了,线程1未执行完任务,线程0未获得锁,所以,线程0退出等待,抛出异常。 

 如果我们把执行任务的时间改成小于线程等待锁的时间 那两个线程就可以正常获得锁了

 结果:

                

   结论:现在2线程个线程都获得了锁,执行了任务,因为在等待时间3s内 别的线程已经释放了锁

         tryLock()不设置等待时间,仅在调用时锁定未被其他线程持有的锁,如果调用方法时, 锁对象对其他线程持有,则放弃. 调用方法尝试获得没,如果该锁没有 被其他线程占用则返回 true 表示锁定成功; 如果锁被其他线程占用 则返回 false,不等待。

        相当于我去上厕所,看到里面有人,我直接扭头就走,不等待,没人我就进去!

例子: 把tryLock(long time, TimeUnit unit)改成 tryLock()其余和上面一样

public class Test8 {
    //定义可重入锁
    private Lock lock=new ReentrantLock();

    public static void main(String[] args) {

        Test8 t8=new Test8();
        //线程0
        new Thread(new Runnable() {
            @Override
            public void run() {
                t8.doThing();
            }
        }).start();
        
        //线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                t8.doThing();
            }
        }).start();

    }

    //定义线程要做的任务
    public void doThing() {
        try {
            System.out.println(Thread.currentThread().getName()+"尝试获得锁....");
            //如果锁被别的线程占用,不等待
            if (lock.tryLock()){//返回值为布尔值 true 或 false
                System.out.println("成功");
                System.out.println(Thread.currentThread().getName()+"获得锁,正在执行任务...");
                Thread.sleep(2000);//模拟执行任务花了4秒
            }else {
                System.out.println(Thread.currentThread().getName()+"获取锁失败!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

 结果:

 结论:发现使用tryLock()线程未获得锁不会等待,就直接退出了,符合我们的理解

1.2.6 newCondition()方法

        关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实 现等待/通知模式。

         Lock 锁的 newContition()方法返回 Condition 对 象,Condition 类也可以实现等待/通知模式. 使用 notify()通知时, JVM 会随机唤醒某个等待的线程,使用 Condition 类可以进行选择性通知。Condition 比较常用的两个方法:

        await()会使当前线程等待,同时会释放锁,当其他线程调用 signal() 时,线程会重新获得锁并继续执行。

         signal()用于唤醒一个等待的线程

注意:

        在调用 Condition 的 await()/signal()方法前,也需要线程持有相 关的 Lock 锁. 调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

例子:创建两个Condition对象实现对指定锁的唤醒

public class Test9 {
    //创建可重入锁
   static Lock lock=new ReentrantLock();
   //创建两个condition对象
   static Condition conditionA=lock.newCondition();
   static Condition conditionB=lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Test9 t9=new Test9();
        //创建2个线程分别调用 waitA() waitB()
        new Thread(new Runnable() {
            @Override
            public void run() {
                t9.waitA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                t9.waitB();
            }
        }).start();
        //main休眠4秒
        Thread.sleep(4000);

        //释放线程A,注意 必须得获得锁才能使用 conditionA.signal();,不不然会产生异常
        lock.lock();
        System.out.println("释放线程A");
        conditionA.signal();
        lock.unlock();
        
        //释放线程B
        lock.lock();
        System.out.println("释放线程B");
        conditionB.signal();
        lock.unlock();



    }

    //创建让线程A等待的方法
    public void waitA(){
        try {
            lock.lock();
            //conditionA利用await()让A线程等待
            System.out.println(Thread.currentThread().getName()+"进入等待状态");
            conditionA.await();
            System.out.println(Thread.currentThread().getName()+"被释放");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    //创建让线程B等待的方法
    public void waitB(){
        try {
            lock.lock();
            //conditionB利用await()让B线程等待
            System.out.println(Thread.currentThread().getName()+"进入等待状态");
            conditionB.await();
            System.out.println(Thread.currentThread().getName()+"被释放");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }

}

结果: 这里的Thread-0 是线程A  Thread-1是线程B

 注意  必须得获得与conditionA.await()的同一把锁,才能使用 conditionA.signal();,不不然会产生异常 :

 

 结果:

1.2.7  公平锁与非公平锁

        大多数情况下,锁的申请都是非公平的, 如果线程1与线程2都在请求 锁 A, 当锁 A 可用时, 系统只是会从阻塞队列中随机的选择一个线程, 不能保证其公平性, 公平的锁会按照时间先后顺序,保证先到先得, 公平锁的这一特点不会出现线程饥饿现象。

        synchronized 内部锁就是非公平的。

        ReentrantLock 重入锁提供了一个 构造方法:ReentrantLock(boolean fair) ,当在创建锁对象时实参传递 true 可以把该锁设置为公平锁.。公平锁看起来很公平,但是要实现公平锁必须要求系统维护一个有序队列,公平锁的实现成本较高,性能也低. 因此默认情况下锁是非公平的. 不是特别的需求,一般不使用公平锁。

公平锁与非公平锁的线程线程运行对比:

不公平锁的情况:

public class Test7 {
    static Lock lock=new ReentrantLock();
    static int num=0;

    public static void main(String[] args) {
        MyRunnable1 myRunnable1=new MyRunnable1();
        Thread t1=new Thread(myRunnable1);
        Thread t2=new Thread(myRunnable1);
        Thread t3=new Thread(myRunnable1);
        Thread t4=new Thread(myRunnable1);

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }

    public void doThing(){
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"开始工作...."+(++num));


            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

    }

    static class MyRunnable1 implements Runnable{
        Test7 t7=new Test7();
        
        @Override
        public void run() {
            for (int i=0;i<5;i++){
                t7.doThing();
            }

        }
    }


 }

结果:有的线程还没工作完 别的线程就把锁抢占了,因为在cpu中获得锁的情况是随机的,不公平的

 

 公平锁的情况:在创建Lock的时候加入   true  即可

 结果:

 每个线程都可以公平的获得锁的机会,不存在有的线程有优先级的情况,是比较公平的

1.2.8  ReentrantLock自己的几个常用的方法

int getHoldCount() 返回当前线程调用 lock()方法的次数

int getQueueLength() 返回正等待获得锁的线程预估数

int getWaitQueueLength(Condition condition) 返回与 Condition 条件 相关的等待的线程预估数

boolean hasQueuedThread(Thread thread) 查询参数指定的线程是否 在等待获得锁

boolean hasQueuedThreads() 查询是否还有线程在等待获得该锁

boolean hasWaiters(Condition condition) 查询是否有线程正在等待 指定的 Condition 条件

boolean isFair() 判断是否为公平锁

boolean isHeldByCurrentThread() 判断当前线程是否持有该锁

boolean isLocked() 查询当前锁是否被线程持有

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值