多线程锁的分类和概述

前言:前面的内容中我们一直在讲锁,其实多线程的关键问题就是在线程安全,而保障线程安全的方式一般有两种,一种就是加锁,另一种则是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时间可能会被白白浪费,所以要根据实际情况使用。

posted @ 2020-05-23 10:28  穿黑风衣的牛奶  阅读(1209)  评论(0编辑  收藏  举报