https://www.jianshu.com/p/d2ac26ca6525
悲观锁与乐观锁:
悲观锁(多锁,适合写多,竞争激烈的场景)
悲观(先取锁再访问):每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
- MySQL中的应用:
使用悲观锁,必须关闭MySQL数据库的自动提交属性。因为MySQL默认使用autocommit模式,也就是说,当执行一个更新操作后,MySQL会立刻将结果进行提交(sql:set autocommit=0)。注意,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。
begin;
// 查询上锁
select quantity from items where id=1 for update;
// 修改库存
update items set quantity where id =1;
// 提交
commit;
在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
- 悲观锁主要分为共享锁或排他锁
(1)共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
(2)排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
乐观锁(少锁,适用于写少,竞争不激烈的应用场景,可以提高吞吐量)
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
- 乐观锁的例子:
select quantity from items where id=1;
// 判断不变再修改
update items set quantity =2 where id =1 and quantity =3;
因为update操作会上排他锁
- 乐观锁的一些实现方法:
(1)使用数据版本(Version++)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
update item;
set quantity=quantity-1 where id =1 and quantity - 1 > 0
// 防止商品库存为0依旧-1,quantity-1>0 相当于乐观锁,相比跟原值比较更适合高并发场景,因为原值修改太频繁
(2)使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
(3)使用原值与当前值对比,类似之前提的。
- MySQL中的注意要点:
InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
update操作会对操作对象加排他锁,保证id对应的行或者表无法被其他线程修改。
- ABA问题:
一个线程A从数据库中取出库存数a==3,这时候另一个线程B也从数据库中取出库存数a==3,并且B进行了一些操作a变成了2,
然后B又将库存数a变成3,这时候线程A进行CAS操作发现数据库中a仍然是3,然后A操作成功。尽管线程A的CAS操作成功,
但是不代表这个过程就是没有问题的。
JAVA中的乐观锁与悲观锁:
- CAS(一种乐观锁):比如Atomic,更新一个变量的时候,只有当变量的原值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。(CAS 基于硬件实现,不需要进入内核,不需要切换线程)
问题1:CPU开销较大(线程冲突严重时乐观锁的自旋)
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
问题2:不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
- synchronized(一种悲观锁):用于修饰类,代码块,方法
Synchronized会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
// 对象锁
public class Test
{
// 对象锁:形式1(方法锁)
public synchronized void Method1()
{
System.out.println("我是对象锁也是方法锁");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 对象锁:形式2(代码块形式)
public void Method2()
{
synchronized (this)
{
System.out.println("我是对象锁");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。
public class Test
{
// 类锁:形式1
public static synchronized void Method1()
{
System.out.println("我是类锁一号");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 类锁:形式2
public void Method2()
{
synchronized (Test.class)
{
System.out.println("我是类锁二号");
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
公平锁与非公平锁:
在非公平锁策略之下,不一定First in First Serve,而是出现各种线程随意抢占的情况。默认的ReentrantLock对象构造出来就是非公平的。当线程被挂起排队等待的时候,会被其他线程趁着线程还未被唤醒的空挡抢占锁(将state置为1)。
在构造ReentrantLock对象的时候传入一个true即可获得公平锁。公平锁会在抢占前先查询队列中是否有线程在等待,如果有就加入等待队列,如果没有就直接执行。
ReentrantLock lock = new ReentrantLock(true)
- 优劣:在CPU线程状态切换的空挡期较长的情况下,非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
自旋锁:
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
互斥锁 mutex:
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。
加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态,
第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。
在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
读写锁 rwlock:
(也叫作共享互斥锁:读模式共享,写模式互斥)
- 适用场景:
读写锁非常适合对数据结构读的次数远远大于写的情况。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。(这也是它能够实现高并发的一种手段) 但是进行读操作的时候不能进行写操作。所以当读者源源不断到来的时候,写者总是得不到读写锁,就会造成不公平的状态。
当处于读模式的读写锁接收到一个试图对其进行写模式加锁操作时,便会阻塞后面对其进行读模式加锁操作的线程。 这样等到已经加读模式的锁解锁后,写进程能够访问此锁保护的资源。
- 解决方案:
当处于读模式的读写锁接收到一个试图对其进行写模式加锁操作时,便会阻塞后面对其进行读模式加锁操作的线程。 这样等到已经加读模式的锁解锁后,写进程能够访问此锁保护的资源。
RCU锁(Read-Copy Update):
读-复制 更新
RCU中,读者不需要使用锁,要访问资源尽管访问就好了。
RCU中,写者的同步开销比较大,要等到所有的读者都访问完成了才能够对被保护的资源进行更新。
写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。
读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。
Java的偏向锁和轻量级锁
(1)重量级锁:
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。