[基础课]--[锁]--锁分类mp.weixin.qq.com
前言
Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。
锁分类
Java中出现的一些锁的基本概念如图所示:
乐观锁 和 悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度.在Java和数据库中都有此概念对应的实际应用.
悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改.Java中,synchronized关键字和Lock的实现类都是悲观锁.
乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据.如果这个数据没有被更新,当前线程将自己修改的数据成功写入.如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试).
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的.
根据从上面的概念描述我们可以发现:
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:
通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。
自旋锁 VS 适应性自旋锁
在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。我们都知道阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。这时候为了优化这种消耗,就引入了自旋锁.当有线程持有锁的时候,另一个线程不会直接挂起,而是等一下,等待上一个线程执行结束,这样就可以避免线程切换的开销,这就是自旋锁.
从定义中我们也可以看出,自旋锁是有缺陷的,当等待的线程长时间不释放锁的时候,自选等待的效果就很差,甚至会比线程切换更消耗时间.所以我们需要添加一个阈值限制自旋的次数(默认为10,-XX:PreBlockSpin更改),当数量到达后就不再自旋,挂起线程.
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。
公平锁 和 非公平锁
公平锁:多个线程按照申请锁的顺序获取锁.
非公平锁:非顺序获取锁,多个线程获取锁的顺序并不是按照申请锁的顺序、有可能后申请的先获取到锁、或造成优先级反转或者锁饥饿现象.
如图所示,打水的人会排队从管理员处获取打水资格后打水,和队列一样先进先出顺序执行.谁先来谁先拿锁,公平锁.
如图所示,打水的人虽然会排队从管理员处获取打水资格后打水,但可以插队获取锁.非公平锁.
可重入锁 和 非可重入锁
可重入锁:又称递归锁,可以多层获取锁,在外层申请到锁后,内层仍可以使用锁并不会发生死锁(前提是同一个对象或者Class)这种锁被称为可重入锁.如 ReentrantLock、synchronized等
如图就是可冲入锁synchronized的使用
非可重入锁:和可重入锁相反,不可递归调用,递归调用会发生死锁.
独占锁 和 共享锁
独享锁和共享锁是一种概念,类似悲观锁和乐观锁. 独占锁:也叫排他锁,该锁每一次只能有一个线程持有。比如当数据A被线程T加锁后,其他线程不可用再对数据A加任何其他的锁.只有获取该排它锁的线程可以对数据进行查看和修改.JDK中的synchronized和Lock实现类都是独占锁.
共享锁:该锁可以被多个线程共有.如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据.
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享.
分段锁
分段锁 其实是一种锁的设计思想,他并不是一种锁.典型的实现就是ConcurrentHashMap,其并发操作就是使用分段锁来设计实现的以达到高效的并发操作.
结语
本文Java中常用的锁以及常见的锁的概念进行了基本介绍,接下来的章节小哈会对照JDK的源码实现来详解介绍各个类型锁的实现和原理.
其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。