1. 1 不可撤销
  2. 2 小年兽 程嘉敏
  3. 3 手放开 李圣杰
  4. 4 迷人的危险3(翻自 dance flow) FAFA
  5. 5 山楂树之恋 程佳佳
  6. 6 summertime cinnamons / evening cinema
  7. 7 不谓侠(Cover 萧忆情Alex) CRITTY
  8. 8 神武醉相思(翻自 优我女团) 双笙
  9. 9 空山新雨后 音阙诗听 / 锦零
  10. 10 Wonderful U (Demo Version) AGA
  11. 11 广寒宫 丸子呦
  12. 12 陪我看日出 回音哥
  13. 13 春夏秋冬的你 王宇良
  14. 14 世界が终わるまでは… WANDS
  15. 15 多想在平庸的生活拥抱你 隔壁老樊
  16. 16 千禧 徐秉龙
  17. 17 我的一个道姑朋友 双笙
  18. 18 大鱼  (Cover 周深) 双笙
  19. 19 霜雪千年(Cover 洛天依 / 乐正绫) 双笙 / 封茗囧菌
  20. 20 云烟成雨(翻自 房东的猫) 周玥
  21. 21 情深深雨濛濛 杨胖雨
  22. 22 Five Hundred Miles Justin Timberlake / Carey Mulligan / Stark Sands
  23. 23 斑马斑马 房东的猫
  24. 24 See You Again Wiz Khalifa / Charlie Puth
  25. 25 Faded Alan Walker / Iselin Solheim
  26. 26 Natural J.Fla
  27. 27 New Soul Vox Angeli
  28. 28 ハレハレヤ(朗朗晴天)(翻自 v flower) 猫瑾
  29. 29 像鱼 王贰浪
  30. 30 Bye Bye Bye Lovestoned
  31. 31 Blame You 眠 / Lopu$
  32. 32 Believer J.Fla
  33. 33 书信 戴羽彤
  34. 34 柴 鱼 の c a l l i n g【已售】 幸子小姐拜托了
  35. 35 夜空中最亮的星(翻自 逃跑计划) 戴羽彤
  36. 36 慢慢喜欢你 LIve版(翻自 莫文蔚) 戴羽彤
  37. 37 病变(翻自 cubi) 戴羽彤
  38. 38 那女孩对我说 (完整版) Uu
  39. 39 绿色 陈雪凝
  40. 40 月牙湾 LIve版(翻自 F.I.R.) 戴羽彤
夜空中最亮的星(翻自 逃跑计划) - 戴羽彤
00:00 / 04:10

夜空中最亮的星 能否听清

那仰望的人 心底的孤独和叹息

夜空中最亮的星 能否记起

那曾与我同行 消失在风里的身影

我祈祷拥有一颗透明的心灵

和会流泪的眼睛

给我再去相信的勇气

越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请指引我靠近你

夜空中最亮的星 是否知道

那曾与我同行的身影 如今在哪里

夜空中最亮的星 是否在意

是等太阳先升起 还是意外先来临

我宁愿所有痛苦都留在心底

也不愿忘记你的眼睛

哦 给我再去相信的勇气

哦 越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请照亮我向前行 哒~

我祈祷拥有一颗透明的心灵

和会流泪的眼睛 哦

给我再去相信的勇气

哦 越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请照亮我向前行

2021.05.07 多线程之可重入锁

在目前web的开发大环境下,高并发,高可用的应用场景越来越普遍,对我们的要求也越来越要求越高了,为了应对这样超高的要求(比如多线程环境下的数据共享问题),我们必须掌握很多常用的技术方案,比如锁(Lock)(就是在某个方法或资源上加锁,却保同一时间段内只有我们可以访问该资源),这样才能写出更可靠的应用程序,今天我们就一起来看下一个很常用的锁——可重入锁(ReentrantLock)。

在开始今天的内容之前,我们先考虑这样一个场景:我们有一个审核业务,同一级的审核人员有两个,但是业务只能审核一次,不能重复审核。

如上图,如果整个审核方法不加锁的情况下,很可能发生同一笔数据审核两次的情况。因为审核过程会涉及多个步骤,假如第一个人员在查询未审核数据后,进行业务审核(处在第三步),但是尚未提交审核结果,这时候第二个人进来,也是查了未审核数据(第二步),由于第一个人员未提交审核结果,这时候数据依然是未审核,然后第二个人开始审核,这时候第一个人提交了审核结果,然后紧接着第二个人提交审核结果。最后,审核结果就会变成两条。

接下来,我们讲的内容,就是为了解决这样的额应用场景。

一个不加锁的案例

在开始可重入锁的介绍之前,我们先看一个和上面类似的例子,算是简化版:

public class Example {
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
        for (int j = 0; j < 1000; j++) {
            Thread.sleep(10L);
            final int finalJ = j;
            executor.submit(() -> test(finalJ));
        }
        executor.shutdown();
    }

    public static void test(int j) {
        System.out.println("==第" + j + "次调用==start");
        i ++;
        Thread.sleep(20L);
        i ++;
        System.out.println(i);
        System.out.println("==第" + j + "次调用==end");
    }

}

上面这段代码其实就是模拟多线程共享数据(就是这里的i),并对数据进行操作的一个示例,运行结果可以很直观的说明,不加锁的情况下,在一个线程未执行完方法之前,另一个方法也会进入方法执行。按照我们代码的逻辑,应该是先打印start,然后打印i的值,然后再打印end,但是实际情况却并发如此,往往可能是这样的:

上面的运行结果很直观的说明,在第1995次未正常运行结束时,第1996次已经开始了,同样在第1996次未运行完的时候,第1998次都开始了。而且不论你运行多少次,上面的结果都大同小异。

这时候,如果我们将代码调整一下,加上锁,看下会发生什么:

public class Example {
    // 可重入锁
    private static final ReentrantLock mainLock = new ReentrantLock();
    private static int i;
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
        for (int j = 0; j < 1000; j++) {
            Thread.sleep(10L);
            final int finalJ = j;
            executor.submit(() -> testLock(finalJ));
        }
        executor.shutdown();
    }

    public static void testLock(int j) {
        final ReentrantLock reentrantLock = mainLock;
        // 如果被其它线程占用锁,会阻塞在此等待锁释放
        reentrantLock.lock();
        try {
            System.out.println("==第" + j + "次调用==start");
            i ++;
            Thread.sleep(20L);
            i ++;
            System.out.println(i);
            System.out.println("==第" + j + "次调用==end");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 执行完之后必须释放锁
            reentrantLock.unlock();
        }
    }

}

然后我们运行一下:

这时候,你会发现,无论你运行多少次,都是像上面这样规整,也和我们的代码逻辑是一致的,这其实就是加锁的作用,目的就是为了控制资源的访问秩序。

当然,上面的代码其实还是存在问题的,因为在循环中使用线程池本身就是不合理的,当单个线程执行时间较长,for中启动前程前的业务响应比较快的时候(就是这里的Thread.sleep(10L);),所有的压力都会到线程池上,会把线程池的资源耗尽,然后报如下错误:

这时候解决方法有两个,一个就是人为增加线程启动前的业务处理时间,这里就是增加睡眠时间,比如调整到Thread.sleep(20L);;另一个是提高线程中的业务处理效率,只要比前面的业务处理快就行,但是在实际业务中,这个是不可能的;最好的解决方法是重构业务逻辑,想办法把for循环放进线程里面,我之前修复的异步线程问题就用的是这个方法。好了,下面开始理论方面的学习。

什么是可重入锁

可重入锁,顾名思义就是可以重复加锁的一种锁,它是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。

加锁的方式

它的加锁方式有三种,分别是locktrylocktrylock(long,TimeUnit)。上面我们枷锁的方法只是其中一种,也是最简单的。

可以看到ReentrantLock的使用方式比较简单,创建出一个ReentrantLock对象,通过lock()方法进行加锁,使用unlock()方法进行释放锁操作。

使用lock来获取锁的话,如果锁被其他线程持有,那么就会处于等待状态。同时,需要我们去主动的调用``unlock`方法去释放锁,即使发生异常,它也不会主动释放锁,需要我们显式的释放。

使用trylock方法获取锁,是有返回值的,获取成功返回true,获取失败返回false,不会一直处于等待状态。

使用trylock(long,TimeUnit)指定时间参数来获取锁,在等待时间内获取到锁返回true,超时返回false。还可以调用lockInterruptibly方法去中断锁,如果线程正在等待获取锁,可以中断线程的等待状态。

总结

关于锁这一块,其实内容比较多,涉及的知识也比较杂,不仅包括javasynchronized、原子类、锁等这些线程安全的知识,还包括数据的行级锁、表级锁等内容,如果是分布式应用,还需要考虑分布式锁的实现,这里面还涉及了redis的知识,想要完全掌握还是难度很大的,但是随着我们一点点的学习和应用,你慢慢会掌握很多常用的技术和解决方案,你会更清楚各种锁和技术的应用场景,你会涉及出更优秀的高并发高可用的系统,为了实现这个目标,让我们一起学习,一起遇见更好的自己,加油吧!

项目路径:

https://github.com/Syske/example-everyday

本项目会每日更新,让我们一起学习,一起进步,遇见更好的自己,加油呀

posted @ 2021-05-09 15:37  云中志  阅读(100)  评论(0编辑  收藏  举报