• 19.Lock锁介绍与应用案例


    JUC中,Lock是一个接口,其作用与synchronized类似,都是为了保证线程安全而提前加锁,不同的是Lock只定义了与抢锁和释放锁相关的操作,没有具体实现,而且要用lock()和unlock()将要保护的代码给包起来才可以。其中常用的接口有三个:

    • lock():抢占锁资源方法,如果当前线程没有抢占到锁,则阻塞。

    • trylock():尝试抢占锁资源,如果抢占成功则返回true,否则返回false。

    • unlock():释放锁。

    Lock既然是一个接口,那么必须有具体的实现才可以,常见的有三个:

    1 重入锁ReentrantLock

    ReentrantLock是一个支持重入的排他锁。所谓排他就是同一时刻只允许一个线程获得锁。

    那重入是什么意思呢?意思是如果一个线程获得了锁,那么后面再访问相同的资源时,不需要再加锁就可以访问,一般此时会记录重入次数的。如果还是不清楚,我们看一个例子:

    1. void update(a){
    2. insert(a);
    3. }

    这是我们常见的一种写法,在update某个记录的时候,我们可能在其内部要先做一个插入操作,一般来说,update和insert都需要先对资源进行加锁。如果操作的是同一个资源,比如这里的update对a加锁了,那执行insert时一定还没有释放,那insert该如何获得锁呢?

    此时最好的方式就是让a在执行insert时可以直接获得锁资源,完成之后updat再继续执行,这就是重入的意思,synchronized和ReetrantLock都支持重入。

    重入锁的例子:

    1. public class ReentrantLockExample {
    2. static Lock lock=new ReentrantLock();
    3. private int count = 0;
    4. public void incr(){
    5. lock.lock();
    6. try {
    7. count++;
    8. }finally {
    9. lock.unlock();
    10. }
    11. }
    12. public static void main(String[] args) throws InterruptedException {
    13. ReentrantLockExample atomicExample = new ReentrantLockExample();
    14. Thread[] threads=new Thread[2];
    15. for (int j = 0;j<2;j++) {
    16. threads[j]=new Thread(() -> {
    17. for (int k=0;k<10000;k++) {
    18. atomicExample.incr();
    19. }
    20. });
    21. threads[j].start();
    22. }
    23. threads[0].join();//保证线程执行结束
    24. threads[1].join();
    25. System.out.println(atomicExample.count);
    26. }
    27. }

    上面的例子中,针对count++进行了加锁,从而保证count++在多线程访问下的线程安全性。

    2 读写锁ReentrantReadWriteLock

    ReentrantReadWriteLock表示可以重入的读写锁,那什么是读写锁呢?想象一下,开会的时候领导在画大饼,大家可以同时看,如果两个领导同时画,我们该怎么看呢?这就是读写的实际价值。读的时候可以多个人同时读,而写的时候只能一个人写。另外为了防止写的过程中,读到的信息可能前后不一致,因此一般写的时候也不能读,具体来说读写锁的特征就是:

    • 读/读不互斥,多个线程访问,线程不会阻塞。

    • 读/写互斥,如果一个读一个写,此时就要阻塞一个。

    • ReentrantReadWriteLock写/写互斥,多个线程同时写,要互斥

    看个例子:

    1. public class ReadWriteLockExample1 {
    2. private final Lock lock=new ReentrantLock();
    3. private List<String> dataList=new ArrayList<>();
    4. public void add(String data) {
    5. lock.lock();
    6. try {
    7. dataList.add(data);
    8. }finally {
    9. lock.unlock();
    10. }
    11. }
    12. public String get(int idx){
    13. lock.lock();
    14. try{
    15. return dataList.get(idx);
    16. }finally {
    17. lock.unlock();
    18. }
    19. }
    20. }

    这个例子中,dataList不是线程安全的,为此采用重入加锁机制,但是这里的get()和add()是同一级别的操作。如果get()操作很多,那么add()就会被阻塞。不过get方法本身不会影响到数据,因此对get的加锁是不必要的,因此我们需要一个更合理的加锁方法,这就是读写锁。

    基于读写锁的代码如下:

    1. public class ReadWriteLockExample {
    2. private final ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    3. private final Lock readLock=readWriteLock.readLock();
    4. private final Lock writeLock=readWriteLock.writeLock();
    5. private List<String> dataList=new ArrayList<>();
    6. public void add(String data) {
    7. writeLock.lock();
    8. try {
    9. dataList.add(data);
    10. }finally {
    11. try {
    12. Thread.sleep(5000);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. writeLock.unlock();
    17. }
    18. }
    19. public String get(int idx){
    20. readLock.lock();
    21. try{
    22. return dataList.get(idx);
    23. }finally {
    24. readLock.unlock();
    25. }
    26. }
    27. }

    上面代码中,通过读锁readLock和writeLock把读写的操作做了分离,从而减少读锁带来的竞争,其具体实现原理,我们后面章节再看。

    3 StampedLock

    ReentrantReadWriteLock有个问题,就是如果有get()操作,那么所有的add()必须等待,也就是读的过程中是不允许写的,而如果get()过多,就会导致写线程一直被阻塞。为了解决这个问题,Jdk8引入了StampedLock锁,对读写访问进行了优化,其基本原理是提供了一种乐观的策略,当有线程调用get()方法读取数据时,不会阻塞准备执行写操作的过程,具体使用方法如下:

    1. public class StampedLockExample {
    2. private int balance;
    3. private StampedLock lock = new StampedLock();
    4. public StampedLockExample() {
    5. balance=10;
    6. }
    7. public void conditionReadWrite (int value) {
    8. // 首先判断balance的值是否符合更新的条件
    9. long stamp = lock.readLock();
    10. while (balance > 0) {
    11. long writeStamp = lock.tryConvertToWriteLock(stamp);
    12. if(writeStamp != 0) { // 成功转换成为写锁
    13. stamp = writeStamp;
    14. balance += value;
    15. break;
    16. } else {
    17. // 没有转换成写锁,这里需要首先释放读锁,然后再拿到写锁
    18. lock.unlockRead(stamp);
    19. // 获取写锁
    20. stamp = lock.writeLock();
    21. }
    22. }
    23. lock.unlock(stamp);
    24. }
    25. public void optimisticRead() {
    26. long stamp = lock.tryOptimisticRead();
    27. int c = balance;
    28. // 这里可能会出现了写操作,因此要进行判断
    29. if(!lock.validate(stamp)) {
    30. // 要重新读取
    31. stamp = lock.readLock();
    32. try{
    33. c = balance;
    34. }
    35. finally{
    36. lock.unlockRead(stamp);
    37. }
    38. }
    39. System.out.println("读取的值为:"+c);
    40. }
    41. public void read () {
    42. long stamp = lock.readLock();
    43. lock.tryOptimisticRead();
    44. int c = balance;
    45. System.out.println("读取的值为:"+c);
    46. // ...
    47. lock.unlockRead(stamp);
    48. }
    49. public void write(int value) {
    50. long stamp = lock.writeLock();
    51. balance += value;
    52. lock.unlockWrite(stamp);
    53. }
    54. public static void main(String[] args) {
    55. StampedLockExample demo=new StampedLockExample();
    56. new Thread(new Runnable() {
    57. @Override
    58. public void run() {
    59. while(true){
    60. demo.read();
    61. demo.optimisticRead();
    62. try {
    63. Thread.sleep(1000);
    64. } catch (InterruptedException e) {
    65. e.printStackTrace();
    66. }
    67. }
    68. }
    69. }).start();
    70. new Thread(new Runnable() {
    71. @Override
    72. public void run() {
    73. while(true){
    74. demo.write(2);
    75. demo.conditionReadWrite(3);
    76. try {
    77. Thread.sleep(1000);
    78. } catch (InterruptedException e) {
    79. e.printStackTrace();
    80. }
    81. }
    82. }
    83. }).start();
    84. }
    85. }

  • 相关阅读:
    tailwindcss引入项目的正确‘姿势’
    Wonderware 实时库——一套可落地的传统工业实时库
    10、String、StringBuilder、StringBuffer的区别
    1009 说反话【PAT (Basic Level) Practice (中文)】
    前端一次性发送几十个请求给后端
    ConnectTimeout和ReadTimeout所代表的意义
    vue用户点击后下载前端项目中的文件
    JSR303数据校验及多环境切换
    54. 螺旋矩阵
    Codeforces Round #818 (Div. 2)
  • 原文地址:https://blog.csdn.net/xueyushenzhou/article/details/126714687