Java、MySQL开发各种锁

简介

为什么用锁?
保障安全。
注:本文局限于Java语言和MySQL数据库。

Java

具体来说,是用于并发情况下的安全,也是为了解决内存中的一致性,原子性,有序性三种问题。

乐观锁和悲观锁

悲观锁

悲观锁,它觉得每次访问数据都可能被其他线程修改,故而在访问资源时就会对资源进行加锁,用这种方式来保证资源在访问时不会被其他线程修改。因此,其他线程想要获取资源的话就只能阻塞,等到当前线程释放锁后在获取。悲观锁保证资源同时只能一个线程进行操作。
实现有synchronized关键字和Lock的实现类。

乐观锁

乐观锁,并不会觉得访问数据时会被修改,不会上锁,但是在提交时会去判断一下是否有别的线程修改当前数据。实现如:并法包下提供的原子类。原理:CAS。
在这里插入图片描述

公平锁和非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,所以能会造成优先级反转或者饥饿现象;synchronized 就是非公平锁,ReentrantLock(使用 CAS 和 AQS 实现) 通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁;非公平锁的吞吐量性能比公平锁大好。

可重入锁

又叫递归锁,指在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁,synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁。

分段锁

实质是一种锁的设计策略,不是具体的锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效并发操作;当要 put 元素时并不是对整个 HashMap 加锁,而是先通过 HashCode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在的不是同一个分段,就做到真正的并行插入,统计 size 时就需要获取所有的分段锁才能统计;分段锁的设计是为了细化锁的粒度。

独占锁和共享锁

独占锁,又叫独享锁,指该锁一次只能被一个线程持有;
共享锁,可以被多个线程持有;
synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁;ReentrantLock 的独享锁和共享锁也是通过 AQS 来实现的。

可中断锁

可中断建立在阻塞等待中断,运行中是无法中断的。synchronized 是不可中断的,Lock 是可中断的。

偏向锁

偏向锁,Biased Locking。大多数情况下,锁不仅不存在多线程竞争,而且总是有同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单的测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。

但是一旦有第二条线程需要竞争锁,偏向模式立即结束,进入轻量级锁的状态。

优点:偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竞争,那偏向锁就是多余的。偏向锁可以通过虚拟机的参数来控制它是否开启。
偏向锁默认开启,使用-XX:UseBiasedLocking禁用。偏向锁可以提高缓存命中率,但偏向锁也需要记录一些信息,有时候性能会更糟,比如使用某些线程池,同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。

偏向锁&轻量级锁

与轻量级锁的区别:轻量级锁是在无竞争的情况下使用 CAS 操作来代替互斥量的使用,从而实现同步;而偏向锁是在无竞争的情况下完全取消同步。
与轻量级锁的相同点:都是乐观锁,都认为同步期间不会有其他线程竞争锁。

互斥锁、自旋锁

互斥锁

锁,默认即是互斥的,又叫阻塞锁。线程的阻塞(WAITING ,挂起)和唤醒(RUNNABLE ,恢复)需要CPU从用户态转为核心态,涉及上下文切换、CPU 抢占等开销。在许多应用中,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。引入自旋锁。

自旋锁

自旋锁是相对于互斥锁的概念。所谓自旋锁,即让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。等待就是执行一段无意义的循环即可(自旋)。自旋锁的线程一直是 RUNNABLE 状态的,一直在那循环检测锁标志位,机制不重复。但是自旋锁加锁全程消耗 CPU,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长。自旋等待的时间,即次数,不能太大。
自旋锁在JDK 1.4.2中引入,默认关闭,可使用-XX:+UseSpinning开启。

自适应自旋锁
在JDK1.6中默认开启。自旋默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。并且加以优化,即自适应自旋锁。自适应,即自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

当一个线程获取锁时,这个锁已经被其他人获取到,那么这个线程不会立马挂起,反而在不放弃CPU使用权的情况下会尝试再次获取锁资源。自旋锁是不公平锁。
优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

区别

最大的区别就是,到底要不要放弃处理器的执行时间。两者都要等待获得共享资源。阻塞锁是放弃CPU时间,进入等待区,等待被唤醒。而自旋锁是一直自旋在那里,时刻的检查共享资源是否可以被访问。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。

如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。互斥锁适用于临界区操作耗时比较长,自旋锁并发量比较高且临界区的操作耗时比较短。

mutex 锁?

轻量级锁&重量级锁

轻量级锁

重量级锁

指的是锁的粒度。粒度越小,越轻量级,性能越好。

锁消除&锁粗化

锁消除

指JIT 编译器在运行时,对一些没有必要同步的代码却同步的锁进行消除。一种彻底的锁优化。通过锁消除,可以节省毫无意义的请求锁时间。

锁消除涉及一个技术:逃逸分析。逃逸分析就是观察某一个变量是否会逃出某一个作用域。

锁粗化

原则上,在编写代码时,总是推荐将同步块(锁粒度)尽可能的小。这样是为了使得需要同步的操作数量小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
大部分情况下,这个原则是正确的。如果如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁的同步操作也会导致不必要的性能损耗。
如果虚拟机探测到很多零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。即加大同步块。

锁升级

锁膨胀,

数据库

行锁,表锁

锁粒度:

  • 表锁开销小,加锁快;不会出现死锁;锁粒度大,发生锁冲突概率高,并发度低;
  • 行锁开销大,加锁慢;会出现死锁;锁粒度小,发生锁冲突的概率低,并发度高;

不同的存储引擎支持的锁粒度是不一样的:

  • InnoDB行锁和表锁都支持
  • MyISAM只支持表锁

行锁又分共享锁和排他锁。
(当然,说的是InnoDB引擎)行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
行级锁的缺点是:由于需要请求大量的锁资源,所以速度慢,内存消耗大。

表锁下又分为两种模式:表读锁(Table Read Lock)、表写锁(Table Write Lock)
在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞。读锁和写锁是互斥的,读写操作是串行。
如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在MySQL里边,写锁是优先于读锁的。写锁和读锁优先级,可以通过参数调节的:max_write_lock_count和low-priority-updates。

乐观锁

不是数据库层面上的锁,是需要自己手动去加的锁。通过版本号(时间戳),加字段的方式实现。这个思想很关键。

悲观锁

共享锁和排它锁是悲观锁的不同的实现,都是行级锁。
要使用悲观锁,需要关闭MySQL数据库的自动提交属性,因为MySQL默认使用autocommit模式,即执行一个更新操作后,MySQL会立刻将结果进行提交。
设置MySQL为非autocommit模式:set autocommit=0;

共享锁

share lock,也叫read lock,S锁,读锁。读取操作创建的锁。共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。

其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据。

在查询语句后面加上lock in share mode,MySQL会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用共享锁的表,而且这些线程读取的是同一个版本的数据。
加上共享锁后,对于update,insert,delete语句会自动加排它锁。

排它锁

exclusive lock,也叫writer lock,X锁,写锁。排它锁指对于多个不同的事务,对同一个资源只能有一把锁。若事务 1 对数据对象A加上X锁,事务 1 可以读A也可以修改A,其他事务不能再对A加任何锁,直到事物 1 释放A上的锁。这保证了其他事务在事物 1 释放A上的锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁。

读取为什么要加读锁呢:防止数据在被读取的时候被别的线程加上写锁。

对于update, insert, delete语句会自动给涉及到的数据集加排它锁。执行语句后面加上for update就可以。
执行事务时关键字select…for update会锁定数据,防止其他事务更改数据。但是锁定数据也是有规则的。查询条件与锁定范围:

  1. 具体的主键值为查询条件
    比如查询条件为主键ID=1等等,如果此条数据存在,则锁定当前行数据,如果不存在,则不锁定。
  2. 不具体的主键值为查询条件
    比如查询条件为主键ID>1等等,此时会锁定整张数据表。
  3. 查询条件中无主键
    会锁定整张数据表。
  4. 如果查询条件中使用了索引为查询条件
    明确指定索引并且查到,则锁定整条数据。如果找不到指定索引数据,则不加锁。

间隙锁GAP

当用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做间隙,GAP。InnoDB对间隙加锁,即间隙锁。间隙锁只会在Repeatable read隔离级别下使用。
间隙锁的目的:

  1. 为了防止幻读(Repeatable read隔离级别下再通过GAP锁即可避免幻读)
  2. 满足恢复和复制的需要

MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读

总结

在这里插入图片描述

  1. MyISAM存储引擎执行SQL语句自动加锁。查询语句给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT等)给涉及的表加写锁,这个过程并不需要用户干预;
  2. 乐观锁其实是一种思想;
  3. 悲观锁用的就是数据库的行锁;

参考

很全面的InnoDB锁机制

原创文章 131 获赞 175 访问量 32万+

猜你喜欢

转载自blog.csdn.net/lonelymanontheway/article/details/102307772