锁
锁模型
在前面JMM内存模型中有介绍过,其实原子性问题的源头是线程切换。如果我们把线程切换禁用了呢?单核CPU场景下好办,因为同一时刻单核就意味着只有一个线程执行,紧致CPU中断,那么操作系统就不会重新调度线程,也就禁止了线程切换。但是多核场景下,同一时刻可能有两个线程同时在执行。
同一时刻只有一个线程在执行这个条件就是我们说的互斥,如果不管在单核CPU还是多核CPU,我们都保证对共享变量的修改是互斥的,那么也就可以保证原子性了。
互斥的方案就是加锁,简单的锁模型如下:
临界区
在图中演示的需要被保护起来,互斥执行的代码就被称为临界区。
但上面的模型还有问题,这就是我们常说的,解铃还须系铃人,不然就好比我去上班了,走的时候把家里门锁上了,然后小红来了居然可以打开我家的门锁,这显然是不合理的。我家的门只有我能打开,这才是正确的做法,改进后的锁模型是这样的:
我们先对临界区需要受保护的资源R,为它创建一把锁LR,然后针对这把锁在进出临界区的时候,加上加锁与解锁操作。
锁分类概念介绍
锁从互斥与非互斥的角度可以分为乐观锁与悲观锁,从线程是否可以重新获取锁对象分为可重入锁与非可重入锁,从等候线程的排队角度,将锁分为公平锁与非公屏锁,从是否允许多个线程获取锁资源,分为共享锁与排他锁,从获取锁方式的角度分为阻塞锁与自旋锁。
下图个人总结,如有误请各位大佬留言指正
乐观锁与悲观锁
悲观锁:
悲观锁,从名字上理解就是悲观的认为一定会有人跟你抢占资源,那么它拿到资源了,不管有没有人,都会把它锁起来,除了它自己,谁都别想获取到这个资源。
Java中的实现
(仅仅是举例嗷,不是说Java中就这么几个)
- synchronized
- ReentrantLock
乐观锁:
乐观锁,则是十分乐观的认为没有人跟它抢这个资源,也就不会对它加锁。那么它怎么保证数据的安全性呢?实际上在它拿到资源时,会打上一个标记,在修改时检查一下这个标记,如果没被人改过,那么就意味着没有人来修改过这个资源,它就会提交修改,如果发现不一致被人修改过,那么反复尝试,直到成功为止。
Java中的实现
- Unsafe
- 原子类
数据库的实现
在数据库中,也是存在并发问题的。多个线程同时修改同一个表的同一行数据,如果什么都不处理,很有可能会出现一个事务读取另一个事务修改、但是未提交的数据(脏读);又或者事务1在读取这行数据,事务2随后对它进行了修改,事务1再次读取就会发现两次读取结果不一致(不可重复读);又或者事务1对一行数据进行修改,同时事务2插入了一条新数据,恰巧部分数据跟事务1修改那行id一致,事务1回头再看就发现似乎刚刚没有修改(幻读)。为此,事务可以设置隔离级别,让用户根据需求选择并发情况下的具体数据不一致容忍方案。
MVCC,并发版本控制就是Mysql的Innodb引擎通过undolog对已提交读与可重复读这两种隔离级别的具体实现方式。
MVCC的实现机制如下:
- innodb为每个表都增加两个隐藏的列:行创建时间与行过期时间,可以简单的理解为存储的就是系统自增生成的版本号。
- 在多线程修改时,只会查找出<=当前行系统版本号的数据,这样就可以确保读取的数据要么是事务开始前就存在的,要么是事务自己插入或者修改的。
通过MVCC,就避免了对行加锁,而且对读非阻塞,并且也解决了乐观锁的ABA问题。
可重入锁与非可重入锁
可重入锁指的是一个线程可以重复获取锁,不会出现死锁;非可重入锁指的就是不允许一个线程重复获取锁
Java中的实现
可重入锁
-
synchronized
-
ReentrantLock
-
ReentrantReadWriteLock
公平锁与非公平锁
公平锁和非公平锁其实指的是锁的等待队列的唤醒机制,如果在唤醒等待线程时,是按照排队顺序,先到先唤醒,那就是公平锁,如果允许插队,后面来的反而先排上队获取到锁了,那就是非公屏锁。
为什么会有非公平锁呢,这其实是为了防止线程唤醒的时候CPU的资源浪费,而且非公平锁并不是无脑的插队,也是有策略的。比如读写锁:
- 写锁可以随时插队
- 读锁仅在等待队列头结点不是想获取写锁的线程时才可以插队
还是挺好理解的啊,如果是无脑的插队,那么就会造成线程饥饿,可以认为非公平锁是在一定程度上公平的插队。
Java中的实现
- ReentrantReadWriteLock:构造函数的入参fair即可指定公平/非公平。
共享锁与排他锁
共享锁指的是允许多个线程同时访问临界区,排他锁就是我占用了临界区你就不能来。实际上排他锁与互斥锁没啥区别,共享锁与排他锁是在特殊场景:读写情况对互斥锁的进一步优化。在读的场景下,其实大家都是来读取,那么如果用了互斥锁就很浪费,因为大家都是读取其实就没必要加锁啊,在写的场景才需要加锁。
Java中的实现
- ReentrantReadWriteLock
数据库的实现
- Mysql的Innodb引擎的读写锁
自旋锁与阻塞锁
自旋锁与阻塞锁是只在获取不到锁时,CPU处理策略的不同。自旋锁是我获取不到锁,那我就一直试图获取锁,CPU一直在这里忙碌;阻塞锁是我获取不到锁,那我就进入阻塞状态,让出CPU的使用权。其实自旋锁就是乐观锁的实现,阻塞锁就是悲观锁的实现。