• 搞明白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可以申请公平锁或者非公平锁,可根据需求定制。

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

    在这里插入图片描述

  • 相关阅读:
    定制事件 观察者模式
    定时器的高级运用 优化
    tamper-proof 对象 nonextensible对象 sealed对象 frozen对象
    函数柯理化
    跨域 Ajax 其他可选技术 异步
    Ajax 跨域 异步 CORS
    原样输出html标签
    JavaScript
    css 中name的用途
    iview 按需引入解决加载慢的问题
  • 原文地址:https://www.cnblogs.com/pjjlt/p/13924110.html
Copyright © 2020-2023  润新知