一:加锁的意义/为什么要加锁
锁应用于多线程并发的场景,用于保证数据的一致性,如果只有单线程那上锁就没有意义.常见的场景就是:电商的库存管理,用来防止库存超发,总库存和sku的库存对不上等问题
二:常说的锁有哪些
数据库:写锁&读锁,行锁&表锁
java:线程锁
概念:乐观锁&悲观锁,独占锁/互斥锁/排他锁&共享锁,公平锁&非公平锁,自旋锁
2.1:读锁&写锁
说到数据库锁就不得不说一下数据库的存储引擎,常见的有MyISAM、InnoDB、BDB、MEMORY、MERGE、EXAMPLE、NDBCluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED等
最常用的有:MyISAM和InnoDB两种,锁的机制也是基于这两种引擎,版本5.5.x
以前是默认的MyISAM,是不支持事务的.5.5.x版本后开始默认使用InnoDB
读锁: --悲观锁 --共享锁
操作(一致):
LOCK TABLE goods READ; 加锁
......
UNLOCK TABLES; 释放锁
MyISAM(不支持事务):
加锁后包含本线程在内的所有线程都只能进行读操作,MyISAM的读取默认就是加读锁
InnoDB:
如果当前线程给当前表加上了读锁,那么其他线程就不能再给当前表加别的锁(也只能加读锁),
当前线程只可以对当前表进行读操作,进行写操作会直接报错.
其他线程也可以对当前表进行读操作,进行写操作会直接阻塞,待当前线程锁释放后写入成功.
写锁: --悲观锁 --排他锁
LOCK TABLE goods WRITE; 加锁
......
show open tables; 查询加锁的表,1为加锁
UNLOCK TABLES; 释放锁
MyISAM(不支持事务):
加锁后本线程只能够对此表进行读,写操作,如果当前操作没有完成就无法进行别的操作.
其他线程无法进行任何操作.
InnoDB:
如果当前线程给当前表加上了写锁,那么其他线程就不能再给当前表加任何锁.
当前线程对该表只能进行写操作.读操作会进入阻塞,直到当前线程释放锁,
其他线程的任何操作都混进入阻塞,直到当前线程释放锁.
读锁&写锁的对比:
总体来说都是进行了锁表,并发量不大的项目中可以使用.
在多线程高并发的情景下不推荐使用(会严重的影响服务器的性能和吞吐量).
2.2:行锁&表锁
行锁: --悲观锁 --排他锁
SET autocommit = 0; //开启事务
SELECT * FROM goods WHERE id = 13 FOR UPDATE; //加锁查询
commit; //提交事务
实现行锁的基本条件是:
1.必须要有事务
2.必须在SELECT语句后面加上FOR UPDATE
3.必须索引有效(InnoDB的行锁是针对SELECT...WHERE字段的索引加的锁,如果没有索引或者索引失效会导致行锁升级为表锁)
表锁: --悲观锁 --读锁&写锁
上面提到的读锁和写锁就是表锁,直接锁定整张表
行锁&表锁的对比:
表锁的优点很明显:开销小,实现逻辑简单,加锁和释放锁都很快,无死锁.
表锁的缺点也很明显,上面也提到过:范围太大,容易发生锁冲突,并发低.
行锁的优点:数据库支持的最小粒度的锁,一次性对一条数据加锁,不易发生锁冲突,并发度高
行锁的缺点:开销比较大,加锁也比较慢,还有可能出现死锁(可能导致系统奔溃),在使用的时候一定要注意
2.3:线程锁
线程锁:
说到线程锁就不得不提JAVA中的@Synchronized:--独占锁 --悲观锁 --非公平锁
在JDK1.6以前Synchronized每次加锁没需要依赖操作系统的Mutex Lock实现,
线程需要切换到内核态,切换成本很高,是重量级锁,并不推荐使用
在JDK1.6之后Synchronized增加了四种锁态:无锁状态,偏向锁状态,轻量级锁状
态,重量级锁状态,会随着竞争逐渐升级
据说是因为sum公司的程序员发现大部分程序大多数时间不会发生多个线程同
时访问静态资源的情况,所以使用了这种策略.可以提高获得锁和释放锁的效率.
偏向锁: 只有一个线程进入临界区
轻量级锁: 多个线程交替进入临界区
重量级锁: 多个线程同时进入临界区
synchronized在线程竞争锁时,首先做的不是直接进入contentionlist队列
排队,在轻量级锁时会尝试自旋获取锁,如果获取不到升级为重量级锁进入
contentionlist队列,这明显对于已经进入队列的线程是不公平的(不公平锁)
在JAVA中除了常见的Synchronized之外还有ReentrantLock,Semaphore,AtomicInteger等
每种锁机制都有各自的适用场景,这里就不一一赘述了,感兴趣的小伙伴可以自行了解
2.4:乐观锁&悲观锁
乐观锁:
每次获取数据的时候都认为别人不会修改,所以不会上锁.但是在最后跟新的时候会去
校验在获取数组到改动期间有没有去更新该数据,常见的使用就是版本号(version)机制
悲观锁:
每次去拿数据的时候都认为别人会修改,在拿数据的时候就会直接上锁,这样其他人操
作该数据时就会进入阻塞
上面的行锁,表锁,读锁,写锁,Synchronized都是典型的悲观锁
乐观锁&悲观锁的对比:
乐观锁也叫做无锁,不加锁的特点能大大提升读操作的性能
悲观锁更适用于写操作多的场景,先加锁可以保证数据的正确性
2.5:独占锁/互斥锁/排他锁&共享锁
独占锁/互斥锁/排他锁(Exclusivelocks简称为X锁):
指的是该锁一次只能被一个线程获得,例如:Synchronized,写锁等
关于这个叫法,因为所处的层面不同(数据库层,程序层等),创建的程序员不同等因素,
就大概分成了这么几种叫法,其实是一个意思.常说的数据库X锁是排它锁,咋叫都行...
共享锁(Share locks简称为S锁):
指的是该锁可以被多个线程锁持有,例如:读锁
独占锁/互斥锁/排他锁&共享锁的对比:
两个都是通过AQS来实现的,根据实现不同的方法,来实现是共享还是独享.
如读锁的共享锁可保证并发度是非常高效的,读写,写读,写写的过程是互斥的
如写锁的排它锁只有自己能持有锁,消耗性能但是是绝对安全
2.6:公平锁&非公平锁
公平锁:
加锁前先查看是否前面已经有排队等待的线程,有的话优先处理排在前面的线程,类似队列
非公平锁:
加锁前先尝试获取锁,获取不到进入队列尾等待
公平锁&非公平锁的对比:
非公平锁相对公平锁性能会高5-10倍,因为公平锁需要在多核情况下维护一个队列,
如果当前线程不是当前队列的第一个,既增加了队列长度,又增加了队列线程的切换数
2.7:自旋锁
自旋锁与互斥锁有点类似,区别就是互斥锁在拿不到锁的时候线程是睡眠的(阻塞),
而自旋锁是保持循环获取锁的这么一种状态.因此才成为"自旋"