悲观锁(synchronized,reentranlock)乐观锁(CAS)
公平锁 非公平锁(synchronized,reentranlock)
可重入锁(synchronized,reentranlock) 不可重入锁
可中断锁(synchronized) 不可中断锁(reentranlock)
自旋锁 非自旋锁
轻量级锁 重量级锁
共享锁(一些读锁,比如Reentrantlock) 独占锁(ReentrantLock,写锁)
分段锁
偏向锁
1.悲观锁和乐观锁
悲观锁就是加锁,乐观锁就是CAS,不上锁,只是比较一下当前值和预期值是否相等
一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。
synchronized
和 ReentrantLock是典型的悲观锁
共享数据加了乐观锁,一个线程操作数据的时候不会上锁,只会判断一下是否有其他线程修改了这个数据,所以乐观锁适合读多写少的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量
乐观锁可以使用版本号机制和CAS算法来实现
以秒杀场景为例:就是比较查询库存时和扣减库存时的stock是不是一样的:
版本号机制:被修改一次版本号就加一
id stock version
10 1 1
查询库存的时候stock版本如果等于扣减库存时stock的版本,说明stock没有被修改过,=
CAS机制:不额外增加一个字段来记录stock的版本,只是比较查询库存时和扣减库存时的stock是不是一样的就行
2.独占锁和共享锁
(1)独占锁
是指锁一次只能被一个线程所持有。独占锁也叫写锁,排他锁,如果一个事务对数据加上独占锁后,那么其他事务无法再获取到这把锁进行读写。获得独占锁的线程即能读数据又能修改数据
synchronized是独占锁
下面这种for update也是独占锁,而且是锁住一行,行级写锁
select * from student where student_id=1 for update
(2)共享锁
是指锁可被多个事务所共享,也叫读锁。当一个事务添加读锁后,其他事务也可以获取这个共享锁,获取到共享锁的事务只能进行读取数据,不能修改数据,只有等读锁释放了,才能写数据
下面这种lock in share mode就是共享锁,而且是行级读锁:
select * from student where name=‘小明’ lock in share mode;
4.公平锁和非公平锁
公平锁
是指多个线程按照申请锁的顺序来获取锁
非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁,但是ReentrantLock可以设置成公平锁:
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);
5.可重入锁
线程获取了锁,进入被锁住的代码段,需要再次获取这把相同的锁
又称为递归锁,一个线程在外层方法获取了锁,进入内层方法会自动获取锁
ReentrantLock和Synchronized都是可重入锁
第一次获取锁之后进入同步代码块,发现里面需要再次获得同一把锁
synchronized(obj)
{
System.out.println(Thread.currentThread().getName()+"第一次获取锁资源");
synchronized(obj)
{
System.out.println(Thread.currentThread().getName()+"第二次获取锁资源");
synchronized(obj)
{
System.out.println(Thread.currentThread().getName()+"第三次获取锁资源");
}
}
}
//打印输出:
t1第一次获取锁资源
t1第二次获取锁资源
t1第三次获取锁资源
如果不可重入,可能导致死锁
下面的代码中对方法A和方法B都加上了锁
如果一个线程调用了methodA,获取了A方法的锁,那就不用再特地去获取方法B的锁,自动获得了方法B的锁
如果是不可入锁, 这个线程不一定能抢到方法B的锁
public synchronized void mehtodA() throws Exception
{
mehtodB();
}
public synchronized void mehtodB() throws Exception
{
}
6.自旋锁
指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋
总结:不挂起而是忙循环(自旋)
自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒很耗资源
7.分段锁
将锁的粒度进一步细化,不是对整个数组加锁,而是仅对数组中的一部分进行加锁
CurrentHashMap 底层就用了分段锁
8.偏向锁
偏向于第一个访问锁的线程
9.行级锁和表级锁,记录锁,间隙锁,临键锁
行级锁和表级锁:MyIsam引擎只支持表级锁,InnoDB引擎既支持表级锁,也支持行级锁
表级锁:mysql中粒度最大的锁,对整张表加锁,实现起来简单,资源消耗也比较少,不会出现死锁,但是由于其粒度太大,触发锁冲突的概率也是最大的,并发写的情况下性能非常差
表级锁针对非索引字段加锁
行级锁:mysql中粒度最小的锁,只对当前操作的行记录(一行或者多行)进行加锁,大大减少了锁冲突的情况,但是加锁的开销也是最大的。而且会出现死锁
行级锁针对索引字段加锁
行锁可以进一步进行分类:记录锁,间隙锁,临键锁,这三个锁都是写锁
(1)记录锁 record locks
仅仅锁住一行
这个就是记录锁
SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;
(2)间隙锁 gap lock
左开右开区间,也就是不包括双端端点
创建一张表,表里面只有两个字段id 和name
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT,
`name` varchar(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test` VALUES ('1', '小罗');
INSERT INTO `test` VALUES ('5', '小黄');
INSERT INTO `test` VALUES ('7', '小明');
INSERT INTO `test` VALUES ('11', '小红');
执行下面的sql语句,发现
BEGIN;
SELECT * FROM `test` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
INSERT INTO `test` (`id`, `name`) VALUES (3, '小张1'); # 正常执行
INSERT INTO `test` (`id`, `name`) VALUES (4, '小白'); # 正常执行
INSERT INTO `test` (`id`, `name`) VALUES (6, '小东'); # 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (8, '大罗'); # 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (9, '大东'); # 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (11, '李西'); # 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (12, '张三'); # 正常执行
COMMIT;
可以看到,(5, 7]、(7, 11] 这两个区间,都不可插入数据,其它区间,都可以正常插入数据。所以所以锁住的是[5,11]
(3)临键锁 next key lock 可以理解为一种特殊的间隙锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据(临键锁只与非唯一索引有关,唯一索引不存在临键锁)
什么时候会加上临键锁:select * from table for update;
或者update XX的时候就会获取到该行记录的临键锁
现在在事务A中执行以下命令:(只开启事务,不提交事务)
start transaction;
update student set email='zm@alibaba-inc.com' where age=26;
更改赵敏的email邮箱 ,此时会锁住的是(26,77]
现在事务B执行下面的命令:
start transaction;
insert into student values("s010","牛牛",28,’女‘,“13344453233”,“2658948489@qq.com”,“1988-02-11“,0);
commit;
发现没有插入成功,因为你插入的age=28,除在被锁住的age=(26,77] 区间内,没有拿到这把临键锁的事务B无法插入成功
只有事务A提交了,才会不锁住这个区间,事务B才能插入成功