我的并发编程(一):java锁的分类

一、概述

    在我们的开发工作中,需要利用多线程处理高并发的情况,那么我们就不可避免的需要用到锁机制。分类总览图如下:

二、锁的分类

    1. 公平锁与非公平锁

    (1) 公平锁:在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问, 其他的每个线程都在等待资源访问的机会,并且遵循先来后到的顺序,这样的锁就叫做公平锁。

    (2)非公平锁:如果针对上诉情况,后来的锁反而比先来的锁先获得资源访问的权限,也就是其他线程获取资源的顺序是随机的,那么对于先来的锁就是不公平的,这样的锁就叫非公平锁。

    公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。利用ReetrantLock类我们可以创建公平锁与非公平锁,只需要在创建的ReetrantLock类的实例的时候,为构造函数传入true或者false。如果是true,则会创建一个ReetrantLock公平锁;如果是false,则会创建一个ReetrantLock非公平锁。ReetrantLock类不仅可以创建公平锁和非公平锁,它还是一把可重入锁,也是一把互斥锁,它具有与 synchronized相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能, 在ReetrantLock类的源码分析中我们可以有更深入的了解。

    2. 独享锁和共享锁

    (1) 独享锁:又叫做排他锁,是指锁在同一时刻只能被一个线程拥有,其他线程想要访问资源,就会被阻塞。JDK中synchronized和JUC中Lock的实现类就是互斥锁。

    (2) 共享锁:锁能够被多个线程所拥有,如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

    ReentrantReadWriteLock类中有两个内部类读锁ReadLock和写锁WriteLock,其中读锁是共享锁,写锁是独享锁。ReadLock和WriteLock是靠内部类Sync实现的锁Sync是继承于AQS子类的,AQS就是并发的根本。

    3. 乐观锁和悲观锁

    (1) 乐观锁:乐观锁总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS算法实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类如AtomicInteger类就是使用了乐观锁的一种实现方式CAS实现的,在学习AtomicInteger类源码的时候会详细讲到。

    (2) 悲观锁:悲观锁认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中的Synchronized关键字和ReentrantLock类等独占锁(排他锁)也是一种悲观锁思想的实现。

    4. 无锁、偏向锁、轻量级锁和重量级锁

    这里涉及一个概念Java对象头,不同状态的锁在对象头中内存分配以及存值都不相同,锁的升级也就是这四种状态的变化,并且变化方向不可逆。Java对象头以及锁升级后面有文章做专门的分析。

    (1) 无锁:即没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。

    (2) 偏向锁: 对象头的分配中看到,偏向锁要比无锁多了线程ID和epoch(epoch作为偏差有效性的时间戳),偏向锁的出现是为了解决只有在一个线程执行同步时提高性能。

    (3) 轻量级锁:轻量级锁是指当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能 。

    (4) 重量级锁:当前线程没有使用CAS成功获取锁,就会自旋一会儿,再次尝试获取,如果在多次自旋到达上限后还没有获取到锁,那么轻量级锁就会升级为 重量级锁。

    锁的4种状态升级过程比较复杂,后面专门出文章详细讲解。

    5. 自旋锁和适应性自旋锁

    (1) 自旋锁:在获取资源的时候,如果资源被其他线程占用,我们就让当前线程“稍等一下”,就是写个while循环让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。

    (2) 适应性自旋锁:

    自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

    自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

    自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock。

    * TicketLock:TicketLock是基于队列的,虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

    * CLHlock:CLHLock是基于链表设计的,是一种基于链表的可扩展,高性能,公平的自旋锁,申请线程只能在本地变量上自旋,它会不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

    * MCSlock:MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

    6. 可重入锁和不可重入锁

    (1) 可重入锁:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

    (2) 不可重入锁:与可重入锁相反,在外层方法获取锁的时候,再进入该线程的内层方法会因为之前已经获取过还没释放而阻塞,导致死锁。

    7. 分段锁

    分段锁是按照锁的颗粒度将数据分段上锁,把锁进一步细粒度化,有助于提升并发效率。ConcurrentHashMap类为了解决HashMap的线程不安全而设计的时候,就是加入了分段锁的概念。可以在《我的jdk源码(二十一):ConcurrentHashMap类》的分析中看到。

三、总结

    学习了java锁的分类以及每种锁对应的实例后,我们可以在源码分析的时候,更加深入的了解源码的构思。

    更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!

猜你喜欢

转载自blog.csdn.net/qq_34942272/article/details/106695405