Skip to content

Latest commit

 

History

History
165 lines (88 loc) · 13.8 KB

locktype.md

File metadata and controls

165 lines (88 loc) · 13.8 KB

锁类型

  1. 我们常说的并发控制,一般都和数据库管理系统(DBMS)有关,在DBMS中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。

    实现并发控制的主要手段大致可以分为 乐观并发控制 和 悲观并发控制 两种。

  2. 无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念

  3. 悲观锁 之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

 但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

  1. 乐观锁( Optimistic Locking ) 是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

    相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

    乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。

CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V 进行比较的值 A 拟写入的新值 B 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

乐观锁的缺点 ABA 问题是乐观锁一个常见的问题

1 ABA 问题 如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. ABA 问题 如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大 自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  2. 只能保证一个共享变量的原子操作 CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与synchronized的使用情景 简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。 补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS

如何选择

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

  1. 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。

  2. 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景

Java 并发编程不可不知的七种锁类型与注意事项

锁是解决并发冲突的重要工具。在开发中我们会用到很多类型的锁,每种锁都有其自身的特点和适用范围。需要深刻理解锁的理念和区别,才能正确、合理地使用锁。

常用锁类型 乐观锁与悲观锁 悲观锁对并发冲突持悲观态度,先取锁后访问数据,能够较大程度确保数据安全性。而乐观锁认为数据冲突的概率比较低,可以尽可能多地访问数据,只有在最终提交数据进行持久化时才获取锁。

悲观锁总是先获取锁,会增加很多额外的开销,也增加了死锁的几率。尤其是对于读操作,不会修改数据,使用悲观锁大大增加系统的响应时间。乐观锁最后一步才提交数据,死锁的几率比较低,但是如果有多个事务同时处理相同数据也有几率会冲突甚至导致系统异常。

传统关系型数据库常常使用悲观锁,以提高数据安全性。使用乐观锁的场景,通常用版本号来确保数据安全。

  • 自旋锁

    自旋锁会让处于等待状态的线程执行空循环一段时间,执行完空循环后如果能够获取锁就立即获取锁,否则才挂起线程。使用自旋锁,能够降低等待线程被挂起的概率。线程进入阻塞状态再次唤醒,需要在用户态和内核态之间进行切换,自旋锁避免了进入内核态,因此有比较好的性能。

    自旋锁适用于竞争不激烈且线程任务执行时间短的场景。但是对于竞争激烈或者任务执行时间长的场景,不适合使用自旋锁,否则会浪费 CPU 时间片。

  • 重入锁

    Java 中提供的可重入锁 ReentrantLock,是一种递归无阻塞的同步机制,可以在外层方法已经加锁的情况下,让内层方法再次获取锁。ReentrantLock 维护了一个计数器,每加锁一次计数器加一,解锁一次计数器减一。Java 中的 synchronized 也是一种可重入锁。

  • 轮询锁与定时锁

    轮询锁是通过线程不断尝试获取锁来实现的,可以避免发生死锁,可以更好地处理错误场景。Java 中可以通过调用锁的 tryLock 方法来进行轮询。tryLock 方法还提供了一种支持定时的实现,可以通过参数指定获取锁的等待时间。如果可以立即获取锁那就立即返回,否则等待一段时间后返回。

  • 读写锁

    读写锁 ReadWriteLock 可以优雅地实现对资源的访问控制,具体实现为 ReentrantReadWriteLock。读写锁提供了读锁和写锁两把锁,在读数据时使用读锁,在写数据时使用写锁。

    读写锁允许有多个读操作同时进行,但只允许有一个写操作执行。如果写锁没有加锁,则读锁不会阻塞,否则需要等待写入完成。

    ReadWriteLock lock = new ReentrantReadWriteLock(); Lock readLock = lock.readLock(); Lock writeLock = lock.writeLock();

锁的使用

减小锁的范围 加锁后可以确保一个方法或一段代码只有一个线程访问,因此锁定范围要尽可能小。比如使用 synchronized 时,能对代码块进行加锁,就尽量不要对方法进行加锁。

对象锁与类锁 能锁对象,就不要锁定类,尽量控制范围。锁定类以后,所有的线程使用同一把锁,同一时刻只有一个线程可以加锁;而锁定对象,可以增加锁的数量,提高并发的效率。

锁的公平性 大部分锁都支持设置公平性:公平锁是指按照线程等待的时间来决定哪个线程先获取锁,非公平锁是指随机选择一个线程来获取锁。重入锁和读写锁默认都是非公平锁,也可以通过参数来设置。使用时需要根据具体场景来决定设置公平或非公平。

锁消除 如无必要,不要使用锁。Java 虚拟机也可以根据逃逸分析判断出加锁的代码是否线程安全,如果确认线程安全虚拟机会进行锁消除提高效率。

锁粗化 如果一段代码需要使用多个锁,建议使用一把范围更大的锁来提高执行效率。Java 虚拟机也会进行优化,如果发现同一个对象锁有一系列的加锁解锁操作,虚拟机会进行锁粗化来降低锁的耗时。

  • 什么是互斥锁?

    在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。 如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。

  • 什么是共享锁?

    互斥锁要求只能有一个线程访问被保护的资源,共享锁从字面来看也即是允许多个线程共同访问资源。

  • 什么是读写锁?

    读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。 读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态

  • 可重入锁

    可重入锁的概念是自己可以再次获取自己的内部锁。举个例子,比如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的(如果不可重入的锁的话,此刻会造成死锁)。说的更高深一点可重入锁是一种递归无阻塞的同步机制。对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

  • 独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock(互斥锁)而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock(读写锁),其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。对于Synchronized而言,当然是独享锁。

  • Java 中概念

-- 同步锁 在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。一般使用 使用synchronized关键字来实现

-- 互斥锁 ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。