• 多线程锁的分类和概述


    前言:前面的内容中我们一直在讲锁,其实多线程的关键问题就是在线程安全,而保障线程安全的方式一般有两种,一种就是加锁,另一种则是CAS,CAS之前已经知道了是什么东西,接下来说一下锁,其实锁也有很多种分类。例如悲观锁,乐观锁等等。。。有助于理解后面的难点

    悲观锁和乐观锁

    一般乐观锁和悲观锁都是在数据库层面的。

    • 悲观锁:悲观锁认为数据会很容易被其他的线程更改,在自己改数据之前,会有其他的线程来改这个数据了,因此在数据处理器,会对整个数据处理过程进行一个互斥锁,禁止其他线程对这个数据进行染指。一般悲观锁都是依靠数据库的锁机制,在操作之前,访问数据的时候先获取锁,如果获取锁失败了,就代表有其他的线程正在修改这个数据,然后等待数据的锁被释放,如果获取成功了,就对数据进行加锁,然后数据操作成功之后,提交事务再释放锁。通过这种方式来保证,同一时刻只有一个线程能进入到这个更新操作来保证数据的一致性

    • 乐观锁:乐观锁跟悲观锁不一样的地方是,乐观锁认为数据一般不会造成冲突,因此在访问记录之前,不会加互斥锁,只有在数据库提交更新的时候,才会检测数据是否冲突,一般常规操作是在数据库中,加一个version字段,在更新操作之前,先查一遍数据库,获取到version字段的值,由于数据库的update操作本身就是原子性的,在更新操作的时候,where条件后加入一个version的比较操作,如果version的值对应上才更新,否则则不更新

      //伪代码思路
      public int update(Entity entity){
       int version = execute('select version from where id = #{entity.id}');
          entity.setVersion(version)
      int count = execute('update table set name=#{entity.name}... version=#{entity.version} where id = {entity.id} and version = #{version}')
              return count;
      }               
                          
      

      如果count的数值为0就代表数据在当前线程改之前已经被其他的线程改过了,因此不执行更新,也可以继续获取数据,通过比较数据继续更新。由于乐观锁在提交的时候才会锁定(因为update的原子性),因此不会产生任何死锁

    公平锁和非公平锁

    公平和非公平锁是根据线程的抢占机制来分的,如果是公平锁,则线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,来的晚的进阻塞队列,可以把公平锁理解成排队,而非公平锁则是大家一起抢,不管你先来后到,谁抢到了算谁的,可以理解成平时咱们挤公交和挤地铁。

    ReentrantLock则提供了公平和非公平锁的实现。说的具体一点就是。如果有三个线程1,2,3,此时线程1持有了锁,2,3线程也都需要获取这把锁,并且2请求比3早,如果是公平锁,那就是2线程获取,3线程先一边待着去,等2用完了他才能用。非公平锁就是,2,3的机会都是一样的,你们俩根据线程调度策略,谁抢着算谁的。

    一般在没有非常需要公平锁的前提下做好使用非公平,因为公平锁的排列方式会带来额外的性能开销。

    可重入锁

    可重入,顾名思义就是可以反复的进入,放到一个线程当中,当一个线程想要获取一个被其他线程已经取得的互斥锁的时候,毫无疑问会被阻塞。但是如果一个线程再次获取他已经持有的锁的时会不会阻塞呢?

    举个具体的例子:

    /**
     * 测试可重入锁
     */
    public class ReSyncDemo {
        public synchronized void m1(){
            System.out.println(" I am M1");
            try {
                //调用m2
                m2();
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public synchronized void m2(){
            System.out.println("I am M2");
        }
    
        public static void main(String[] args) {
            ReSyncDemo reSync = new ReSyncDemo();
            new Thread(() -> {
                reSync.m1();
            }).start();
            
            new Thread(() -> {
                reSync.m2();
            }).start();
        }
    }
    

    输出结果

     I am M1
    I am M2
    I am M2
    

    运行上面的代码,可以观察到 第一个 I am M1和M2几乎是同时输出的。而第二个I am M2却是在两秒后输出的。这一点就可以印证synchronized是可重入锁。因为我们知道,synchronized锁的是对象,当线程1进入m1方法的时候。运行第一个打印,当他运行到调用m2方法的时候,发现是m2是需要持有锁才能访问,但是这个锁已经被自己持有了,就是当前对象的锁。于是可以直接调用。调用完毕之后然后休眠两秒。等待程序结束,线程2才可以去执行。

    关于可重入锁的原理其实是这样的,在锁内部维护一个线程的标志,用来标识该锁是被哪个线程持有的。然后关联一个计数器,当计数器为0的时候,代表该锁没有被任何线程占用。当一个线程获取了锁的时候,计数器会变成1.然后其他线程再来的时候,会发现这个锁已经被其他线程持有了,并且比较这个锁不是自己持有的,于是阻塞挂起。

    但是获取了该锁的线程,再次访问同步方法的时候,例如上面的m1调用m2,跟线程标志比较一下发现这个锁的拥有者是自己。于是就可以直接进入,然后把count+1,释放之后-1,直到计数器为0的时候,代表线程不管重入了多少次,现在都已经全部释放了。然后把线程的标识置为null,然后其他被阻塞的线程就会来抢这个锁

    自旋锁

    关于自旋锁,关于自旋锁其实很多地方都用到了。CAS就是一种自旋锁,在synchronized的锁升级过程,AQS中也用到了自旋锁。在很多锁中,一个线程获取锁失败后,一般都会被阻塞而被挂起。等到线程获取锁的时候,又要把线程唤醒。这种反复的切换开销比较大,于是就出现了自旋锁,自旋锁严格意义上来说不是锁,或者说是一种非阻塞的“锁”,自旋锁的过程是这样的,当前线程获取锁的时候,如果发现这个锁被其他的线程占有,在不放弃cpu使用权的情况下,多次尝试获取(默认是十次,可以更改)。自旋锁认为,自己在这十次获取的过程中,其他线程已经释放了锁。如果指定的次数还没有获取到,当前线程才会被阻塞挂起。所以自旋锁是一种用cpu时间换线程阻塞和调度的开销。但是造成的问题是,如果指定的次数还没有获取到,这些cpu时间可能会被白白浪费,所以要根据实际情况使用。

  • 相关阅读:
    [THUWC2017]在美妙的数学王国中畅游
    添加右键使用 SublimeText 打开
    添加右键使用 SublimeText 打开
    添加右键使用 SublimeText 打开
    安装 wordpress 出现 抱歉,我不能写入wp-config.php文件
    安装 wordpress 出现 抱歉,我不能写入wp-config.php文件
    安装 wordpress 出现 抱歉,我不能写入wp-config.php文件
    使用 Resharper 快速做适配器
    使用 Resharper 快速做适配器
    使用 Resharper 快速做适配器
  • 原文地址:https://www.cnblogs.com/blackmlik/p/12941347.html
Copyright © 2020-2023  润新知