Contents
  1. 1. 基本概念
    1. 1.1. 锁的种类
      1. 1.1.1. 悲观锁
      2. 1.1.2. 乐观锁
      3. 1.1.3. 互斥锁
      4. 1.1.4. 自旋锁
    2. 1.2. CAS操作
    3. 1.3. 锁住什么
  2. 2. 数据库的锁
    1. 2.1. 共享锁和排它锁
      1. 2.1.1. 共享锁【S锁】
      2. 2.1.2. 排他锁【X锁】
    2. 2.2. 行锁、表锁、页面锁
    3. 2.3. 间隙锁
    4. 2.4. 事务中的锁级别
      1. 2.4.1. 事务的加锁逻辑
      2. 2.4.2. 数据库读取数据方式
      3. 2.4.3. 锁级别
      4. 2.4.4. RR适合哪些场景
  3. 3. Java中的锁
    1. 3.1. volatile
      1. 3.1.1. 限定条件
      2. 3.1.2. 使用场景
    2. 3.2. ThreadLocal
    3. 3.3. synchronized
    4. 3.4. AtomicReference
    5. 3.5. ReentrantLock
    6. 3.6. ReadWriteLock

有资源竞争就需要有锁来保证串行化的处理,我们接触最多的是线程的锁,两个线程对同一个变量做自增的操作,如果要每个操作都是有意义的,就必须加上锁来保证串行访问;数据库的锁种类会比较多,还有事务的概念。

设计一个系统的时候,数据本身的特性要考虑好,哪些资源操作需要锁,哪些业务上不加锁可以接受,哪些行为中不加强度比较大的锁,虽然会出问题,但是可以控制住,比如乐观锁

基本概念

锁的种类

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。

乐观锁一般来说有以下几种方式:

  • 使用数据版本(Version)记录机制实现;
  • 使用时间戳(timestamp)
  • 使用原子操作判断当前值

atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增

互斥锁

在拿不到锁资源的时不会占着cpu,与之相反的是自旋锁

自旋锁

自旋锁是不可重入锁。与普通锁不同的是,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。Java中没有实现自旋锁,需要自己实现:

class SpinLock {
//java中原子(CAS)操作
AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象
private int count;
public void lock() {
Thread cur = Thread.currentThread();
//lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环
//一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
while (!owner.compareAndSet(null, cur)){
}
}
public void unLock() {
Thread cur = Thread.currentThread();
owner.compareAndSet(cur, null);
}
}
}

如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的

CAS操作

CAS操作有3个操作数,内存值M,预期值E,新值U,如果M==E,则将内存值修改为B,否则啥都不做。这个操作类似于数据库中update + where的操作,必须在确认的条件下去操作资源,业务中主要作用可以确认哪个线程修改内容成功

/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

也可以用来实现原子的自增操作:

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}

锁住什么

  • Java中synchronized(this)代表锁的id是一个对象引用,锁住的是对应的代码块
  • sql语句中,不能自己创建锁,没有锁的id的概念,锁住的是where后面条件的内容,如果where后面条件不是索引,则锁住整张表

数据库的锁

共享锁和排它锁

锁的粒度越细,更业务场景越匹配,效率就会越高

共享锁【S锁】

select … lock in share mode
又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

排他锁【X锁】

select …for update;update,delete,insert
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制

行锁、表锁、页面锁

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

  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般​

间隙锁

给符合条件的已有数据记录的 索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)

举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

select * from  emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。在事务中使用的比较多

事务中的锁级别

事务的加锁逻辑

事务是一连串的动作,不同的锁类型主要是针对多种锁的情况

事务 加锁/解锁处理
begin;
insert into test ….. 加insert对应的锁
update test set… 加update对应的锁
delete from test …. 加delete对应的锁
commit; 事务提交时,同时释放insert、update、delete对应的锁

事务中,执行语句失败概率越低的语句越放后面,减少无效的操作

数据库读取数据方式

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

  • 快照读:就是select
    • select * from table ….;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;

锁级别

锁级别是指事务A在访问资源时受到事务B对相同资源操作的影响,如下

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交(未执行commit)事务修改的数据,这种级别的并发最高,因为就只有两个事务之间,每个资源操作排它锁的互斥了
  • 提交读(Read Committed):只能读取到已经提交的数据,一个资源的变化依赖与其他事务的commit,不然还是快照读
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,实现方式就是整个事务中读取同一个版本的数据,快照读,乐观锁的机制。在有些RR中,解决了幻读问题,方法就是在查询的时候加入间隙锁
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

一般数据库默认是Read Committed,符合一般的业务逻辑,只有一个事务完成commit后,它的数据才应该生效

RR适合哪些场景

统计查询报表类需求.比如,我们需要根据一些明细,统计数据,并将结果保存到另外几张表中,也就是无法通过一个SQL完成,使用mysql RR就比较合适

begn work

insert into tj1 select * from t where ….

insert into tj2 select * from t where ….

commit

如果使用read commit级别,tj1插入后,t可能被修改新增删除,那么随后的tj2结果就可能与tj1的基础数据不一致.

但是使用RR级别就不存在这个问题.因此第一次读就被固化了.即便t随后被清空,也不影响tj2的结果.

如果没有这种机制,比如要在readcommit级别下完成这样的任务,则只能通过临时表,第一次先把所有需要统计的数据保存到临时表,

随后统计全部在临时表进行.这显然比较麻烦

Java中的锁

volatile

限定条件

只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值,所以自增是不适合的
  • 该变量没有包含在具有其他变量的不变式中

使用场景

  • 变量设置
  • 安全发布,发布完整的类,即类的构造函数执行完
  • 开销较低的读-写锁策略
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}

ThreadLocal

让一个资源变成每个线程私有,这样不用所有线程对这个资源都需要竞争,但会增加空间开销,空间换时间

synchronized

为方法或代码块加锁

AtomicReference

封装了设置对象引用的原子操作,利用CAS操作

ReentrantLock

Java中,synchronized和ReentrantLock是可重入锁,ReentrantLock提供丰富的接口处理一些需要不同锁级别的需求

  • lock.tryLock() : 如果发现该操作已经在执行中则不再执行,return false
  • lock.tryLock(5, TimeUnit.SECONDS): 如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
  • lock.lockInterruptibly(): 如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作

ReadWriteLock

读写锁

版权声明:本文为博主原创文章,转载请注明出处

Contents
  1. 1. 基本概念
    1. 1.1. 锁的种类
      1. 1.1.1. 悲观锁
      2. 1.1.2. 乐观锁
      3. 1.1.3. 互斥锁
      4. 1.1.4. 自旋锁
    2. 1.2. CAS操作
    3. 1.3. 锁住什么
  2. 2. 数据库的锁
    1. 2.1. 共享锁和排它锁
      1. 2.1.1. 共享锁【S锁】
      2. 2.1.2. 排他锁【X锁】
    2. 2.2. 行锁、表锁、页面锁
    3. 2.3. 间隙锁
    4. 2.4. 事务中的锁级别
      1. 2.4.1. 事务的加锁逻辑
      2. 2.4.2. 数据库读取数据方式
      3. 2.4.3. 锁级别
      4. 2.4.4. RR适合哪些场景
  3. 3. Java中的锁
    1. 3.1. volatile
      1. 3.1.1. 限定条件
      2. 3.1.2. 使用场景
    2. 3.2. ThreadLocal
    3. 3.3. synchronized
    4. 3.4. AtomicReference
    5. 3.5. ReentrantLock
    6. 3.6. ReadWriteLock