zoukankan      html  css  js  c++  java
  • 搞明白synchronized和ReetrantLock

    上一篇文章,我们熟悉了Java锁的分类。今天,来学习下Java中常用的悲观锁synchronized和ReetrantLock吧。学习使我快乐,哦耶!

    synchronized

    synchronized是什么?

    synchronized关键字可以保证,一段时间内共享资源只能被一个线程所使用,或者说一段代码一段时间内只能被一个线程执行,并且共享资源对其他线程是可见的。

    实际上,synchronized就是,某个线程拿到一个锁,锁住共享资源,当使用完,放开锁,让其他线程申请锁并使用共享资源。

    synchronized锁的级别

    synchronized作用在普通方法或者代码片段上时,锁为对象本身。作用在static方法或者代码片段上时,锁为类本身

    synchronized的基本使用

    我们设想一个卖票场景,有A、B两个售票窗口卖票,票池(共享资源)只有一个。
    实验1

    public class SellTicketRunnable implements Runnable {
        // 剩余票数
        static int ticket = 1000;
        @Override
        public void run() {
            for (int i=0;i<550;i++){
                sell();
            }
        }
        // 买票操作
        private synchronized void sell() {
            System.out.println(Thread.currentThread().getName()+"开始卖票");
            try {
                // 模拟卖票
                if (ticket <= 0){
                    System.out.println(Thread.currentThread().getName()+"窗口通知,票卖完了~");
                }else {
                    Thread.sleep(5);
                    ticket--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,现在还有"+ticket+"张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"结束卖票");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
            // 代码示例 1 基本使用
            SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
            Thread thread_1 = new Thread(sellTicketRunnable,"A窗口");
            Thread thread_2 = new Thread(sellTicketRunnable,"B窗口");
            thread_1.start();
            thread_2.start();
            thread_1.join();
            thread_2.join();
            System.out.println("运行结束,剩下"+SellTicketRunnable.ticket+"张票");
    }
    

    首先创建售票类SellTicketRunnable,定义公告资源ticket为1000张票,我们每个窗口模拟卖票550张,如果发现票卖完了,就系统提示,否则票数减1。main方法开启两个线程,发现完美运行。发现票数最终为0,并且每个线程访问共享资源的时间内都是独享的。

    A窗口开始卖票
    A窗口窗口通知,票卖完了~
    A窗口结束卖票
    B窗口开始卖票
    B窗口窗口通知,票卖完了~
    B窗口结束卖票
    运行结束,剩下0张票
    

    synchronized对象级别的锁

    刚才只生成了一个SellTicketRunnable,只有一把锁。那我们生成两个SellTicketRunnable对象,会不会有两把锁呢?
    实验2

       SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
       SellTicketRunnable sellTicketRunnable_backups = new SellTicketRunnable();
       Thread thread_1 = new Thread(sellTicketRunnable,"A窗口");
       Thread thread_2 = new Thread(sellTicketRunnable_backups,"B窗口");
       thread_1.start();
       thread_2.start();
       thread_1.join();
       thread_2.join();
       System.out.println("运行结束,剩下"+SellTicketRunnable.ticket+"张票");
    // 运行结果如下:
    A窗口开始卖票
    B窗口开始卖票
    A窗口出票成功,现在还有999张票
    A窗口结束卖票
    A窗口开始卖票
    B窗口出票成功,现在还有998张票
    B窗口结束卖票
    B窗口开始卖票
    A窗口出票成功,现在还有997张票
    A窗口结束卖票
    ...
    运行结束,剩下-1张票
    

    main方法改成上边所示。首先,访问共享资源的时间不再独享。A窗口还没访问完数据库呢,B窗口就去访问了。这最终导致票可能超卖。(就剩1张票了,A、B窗口同时卖出,同时更新共享资源)。当然这段代码你多运行几次才会出现剩余-1的情况,有时候可能为0,毕竟那么巧的事,不是每次都遇到哈。说明,此时锁是对象级别的

    实际上,如果synchronized作用在对象级别上。内存中,对象的对象头会记录当前获取锁的线程,利用的是Monitor机制。

    synchronized类级别的锁

    实验3

    public class SellTicketRunnablePlus implements Runnable {
        // 剩余票数
        static int ticket = 1000;
        @Override
        public void run() {
            for (int i=0;i<550;i++){
                sell();
            }
        }
        public void sell() {
            synchronized(SellTicketRunnablePlus.class){
                System.out.println(Thread.currentThread().getName()+"开始卖票");
                try {
                    // 模拟卖票
                    if (ticket <= 0){
                        System.out.println(Thread.currentThread().getName()+"窗口通知,票卖完了~");
                    }else {
                        Thread.sleep(5);
                        ticket--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,现在还有"+ticket+"张票");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"结束卖票");
            }
        }
    }
    // 运行结果
    A窗口开始卖票
    A窗口窗口通知,票卖完了~
    A窗口结束卖票
    B窗口开始卖票
    B窗口窗口通知,票卖完了~
    B窗口结束卖票
    运行结束,剩下0张票
    

    实验2主函数中的SellTicketRunnable类换成SellTicketRunnablePlusSellTicketRunnablePlus只是给sell方法内部,synchronized锁的是SellTicketRunnablePlus类。此时又是一把锁了,所以两个窗口又可以某段时间内独享共享资源了。

    synchronized是重入锁

    synchronized可以保证不同线程同一时间只能有有一个独享共享资源,比如说线程1持有了锁,线程2去申请锁的时候,发现线程1持有锁呢,所以线程2需要等会(线程阻塞)。那么线程1在持有锁的情况下,可以再申请一把同样的锁吗?
    实验4

    public class ReentryTest {
        public synchronized void outMethod(){
            innerMethod();
            System.out.println("这是外部方法,执行了");
        }
        private synchronized void innerMethod(){
            System.out.println("这是内部方法,执行了");
        }
    }
    // main方法
     ReentryTest reentryTest = new ReentryTest();
     reentryTest.outMethod();
    //运行结果
    这是内部方法,执行了
    这是外部方法,执行了
    

    当线程1执行outMethod方法时,获得了锁。outMethod调用innerMethod方法时,线程1又去申请了同一把锁,发现申请成功了。可重入锁是指同一个线程可以多次加同一把锁。

    自JDK1.6开始,当只有两个线程竞争锁时,synchronized是轻量级锁,超过两个线程竞争的时候是重量级锁。关于锁的分类,请戳链接: Java锁分类原来是这个样子

    在这里插入图片描述

    ReetrantLock

    synchronized是关键字,很多操作都是隐式的,比如说释放锁自旋次数等,都是虚拟机帮你搞定的。为了显示操作,并且拥有更强大的功能,ReetrantLock来了。

    ReetrantLock基本使用

    实验5

            ReentrantLock lock = new ReentrantLock();
            lock.lock();
            try{
                // 业务逻辑
            }catch (Exception e){
            }finally {
                lock.unlock();
            }
    

    ReetrantLock需要手动申请锁和释放锁,分别为方法lockunlock

    ReetrantLock重入性

    synchronized一样,ReetrantLock也具备重入性。
    实验6

            ReentrantLock lock = new ReentrantLock();
            int count = 0;
            for (int i = 1; i <= 3; i++) {
                lock.lock();
                System.out.println("说明获取锁"+ ++count +"次");
            }
            for (int i = 1; i <= 3; i++) {
                lock.unlock();
            }
    //
    说明获取锁1次
    说明获取锁2次
    说明获取锁3次
    

    公平锁和非公平锁

    ReetrantLock可以申请公平锁或者非公平锁(了解锁的分类:Java锁分类原来是这个样子)。

    首先我们补充一个知识点,ReetrantLock是实现AQS机制的,就是说所有申请锁的线程,会被按需放到一个队列中,然后依次获取锁。公平锁保证了,获取锁的顺序性。

    实验7

    //主函数
            ReentrantLock lock = new ReentrantLock(true);
            for (int i=1;i<=5;i++){
                new Thread(new FairLockThread(lock),"第"+i+"个").start();
            }
    // FairLockThread类
    public class FairLockThread implements Runnable {
        ReentrantLock lock;
        public FairLockThread(ReentrantLock lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            for (int i = 1; i <= 2; i++) {
                lock.lock();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "开始执行了");
                System.out.println(Thread.currentThread().getName() + ":" + lock.getQueueLength());
                lock.unlock();
            }
        }
    }
    // 结果
    第1个开始执行了
    第1个:4
    第2个开始执行了
    第2个:4
    第3个开始执行了
    第3个:4
    第4个开始执行了
    第4个:4
    第5个开始执行了
    第5个:4
    第1个开始执行了
    第1个:4
    第2个开始执行了
    第2个:3
    第3个开始执行了
    第3个:2
    第4个开始执行了
    第4个:1
    第5个开始执行了
    第5个:0
    

    ReentrantLocknew的时候传入true,就是申请了一把公平锁。FairLockThread方法里面让一个线程执行两次申请锁、释放锁操作,并且模拟使用锁0.5秒。getQueueLength方法就是查看,当前队列中阻塞的线程数。可以看出,锁的两遍申请是按照顺序的,从1~5。从线程是也可以看出,没有哪个线程可以偷偷的自己两边都执行完。

    还是实验7

    ReentrantLock lock = new ReentrantLock(false);
    // 结果
    第2个开始执行了
    第2个:4
    第2个开始执行了
    第2个:4
    第1个开始执行了
    第1个:3
    第1个开始执行了
    第1个:3
    第3个开始执行了
    第3个:2
    第4个开始执行了
    第4个:2
    第4个开始执行了
    第4个:2
    第5个开始执行了
    第5个:1
    第5个开始执行了
    第5个:1
    第3个开始执行了
    第3个:0
    

    我们只需要将主函数,newReentrantLock的时候设置成false,此时申请的就是非公平锁了。再看运行结果,某个线程执行完第一遍,很大概率上就会执行第二遍。没有按照顺序执行,这是不公平的。

    执行完一遍,然后紧接着执行第二遍,不用切换上下文,某线程一致使用CPU,这样效率更快的,所以非公平锁效率更高

    ReetrantLock可中断,预防死锁问题

    试想一下,如果线程1已经持有锁1,现在想拿锁2,然后就可以开心的结束了。线程2已经持有锁2,现在想拿锁1,然后就可以开心的结束了。这俩线程还愉快的碰面了,结果谁都不放手,谁都不能愉快的结束,于是乎,死锁就产生了。

    实验8

    public class InterruptThread implements Runnable{
        ReentrantLock firstLock;
        ReentrantLock secondLock;
        public InterruptThread(ReentrantLock firstLock, ReentrantLock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                firstLock.lock();
                Thread.sleep(1000);
                secondLock.lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
    // 主函数
     ReentrantLock lock = new ReentrantLock();
     ReentrantLock lock2 = new ReentrantLock();
     Thread a = new Thread(new InterruptThread(lock, lock2), "A");
     Thread b = new Thread(new InterruptThread(lock2, lock), "B");
     a.start();
     b.start();
    //结果
    没有结果...
    

    以上,运行到电脑死机也不会结束了。如果我们在主函数最后一行后面加上一行

    a.interrupt();
    

    运行结果,放个图吧。
    在这里插入图片描述
    可以看出,虽然A牺牲掉了,但是由于A的中断(放弃持有锁1)。B顺利完成了!为小A默哀一分钟。。。

    相同与不同

    相同

    1. synchronizedReetrantLock都是悲观锁、可重入锁。

    不同

    1. synchronized是隐士申请、释放锁,虚拟机层面维护。ReetrantLock是显示操作,代码维护。
    2. 在JDK1.6之前, synchronized性能极差,1.6之后,它俩性能差不多。
    3. ReetrantLock可中断,避免死锁产生。
    4. ReetrantLock可以申请公平锁或者非公平锁,可根据需求定制。

    呜呼,从探索到验证,辣条君用了一天,小伙伴们点个赞再走吧。

    在这里插入图片描述

  • 相关阅读:
    JavaSE 基础 第51节 定义自己的异常
    JavaSE 基础 第50节 Java中的异常链
    JavaSE 基础 第49节 手动抛出异常
    JavaSE 基础 第48节 Java中的异常声明
    JavaSE 基础 第47节 获取异常信息
    JavaSE 基础 第46节 异常的分类
    JavaSE 基础 第45节Java异常快速入门
    JavaSE 基础 第44节 引用外部类的对象
    JavaSE 基础 第43节 静态内部类
    通用爬虫
  • 原文地址:https://www.cnblogs.com/pjjlt/p/13924110.html
Copyright © 2011-2022 走看看