常见的各种锁
一、常见锁简单说明
1、悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,在获取数据的时候先加锁,确保数据的安全性。
锁实现:关键字synchronized、Lock接口的实现
使用场景:写操作比较多,先加锁可以保证写操作时数据正确
2、乐观锁
乐观锁认为自己在使用数据的时候不会被别的线程修改,所以不会添加锁,只是在更新的时候去判断之前有没有别的线程更改过这个数据
锁实现:CAS算法,例如AtomicInteger类的原子自增底层是通过CAS实现的
使用场景:读多,不加锁的特点能够使读的性能大幅度提升
3、读锁(共享锁)
读锁即共享锁(S锁):共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。
4、写锁(排他锁)
写锁即排他锁(X锁):用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
5、行锁
行锁即对数据表中每一行数据加锁,数据库最细粒度的锁,开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
实现:InnoDB
6、表锁
表锁即对数据库中每个表加锁,数据库中最大级别的锁,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
实现:MyISAM、BDB、InnoDB
7、页锁
页锁即对组加锁,对相邻数据加锁,数据库中介于表锁和行锁之间的锁,开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
实现:BDB
8、互斥锁(重量级锁或阻塞同步、悲观锁)
互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它,互斥锁可适用于一个共享资源每次只能被一个线程访问的情况
9、自旋锁(CAS)
自旋锁在申请资源但是申请不到的情况下并不会挂起,而是会选择持续申请。这种锁结果适用于每个线程占用较少时间的锁,并且线程阻塞状态切换的代价远高于等待的代价时使用。
10、分布式锁
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
实现:数据库实现分布式锁; 缓存(Redis等)实现分布式锁; Zookeeper实现分布式锁;
11、区间锁(分段锁)
ConcurrentHashMap jdk1.7使用了分段锁来保证线程安全,效率比起使用synchronized的HashTable要高的很多。每个集合都可以看作是一个存储东西的房子,HashTable与ConcurrentHashMap存储的都是HashEntry数组(每个数组里面是链表,暂且忽略,直到就好)
12、重入锁
重入锁当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的 ,可避免死锁
锁实现:关键字synchronized,ReentrantLock锁实现
13、非重入锁
非重入锁与可重入锁相反,不可递归调用,递归调用就发生死锁。
锁实现:NonReentrantLockk锁实现
14、公平锁
公平锁多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
锁实现:ReentrantLock(true)锁实现
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
15、非公平锁
非公平锁多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:1、你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死2、会发生羊群效应
二、synchronized 关键锁
1、synchronized 锁升级 jdk1.6后做了优化
其实每个java对象都可以是一个锁对象包括对方法加锁,其实JVM底层也是对对象的class进行加锁,java对象在jvm内存中存储分为三部分:
1、对象头(主要是运行时的数据)
2、实例数据
3、填充数据
对象头里面的数据简单如下
长度 内容 说明
从上图可以看出java中每个对象都能成为锁对象,改对象的锁信息存储在jvm内存中的对象头的markword中当创建一个对象后如下图,偏向锁标志位01,状态为0表示改对象还没有加上偏向锁,(1表示改对象加上了偏向锁),即该对象初始化完成后就有了偏向锁的标志位,这也说明了所有的对象是可偏向的,同时也说明了刚创建的对象偏向锁是没有生效的
偏向锁生效
线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
所谓临界区,就是只允许一个线程进去执行操作的区域,即同步代码块。CAS是一个原子性操作
此时的Mark word的结构信息如下:
此时偏向锁的状态为“1”,说明对象的偏向锁生效了,同时也可以看到,哪个线程获得了该对象的锁。
如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
锁膨胀
当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这也是我们经常所说的锁膨胀
锁撤销
由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下
1、在一个安全点停止拥有锁的线程。
2、遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。3、唤醒当前线程,将当前锁升级成轻量级锁。
所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭
轻量级锁
锁撤销升级为轻量级锁之后,那么对象的Markword也会进行相应的的变化。下面先简单描述下锁撤销之后,升级为轻量级锁的过程:
1、线程在自己的栈桢中创建锁记录 LockRecord。
2、将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
3、将锁记录中的Owner指针指向锁对象。
4、将锁对象的对象头的MarkWord替换为指向锁记录的指针。
注:锁标志位”00”表示轻量级锁
轻量级锁主要有两种
1、自旋锁(jdk1.4~jdk1.6之前 默认自旋10次,可以通过jvm参数自行设置) 2、自适应自旋锁(jdk1.6起jvm自己来计算自旋升级逻辑)
自旋锁
所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的空循环。
所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁
自适应自旋锁
所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。
另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。
重量级锁
轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
当轻量级所经过锁撤销等步骤升级为重量级锁之后,它的Markword部分数据大体如下
为什么说重量级锁开销大呢主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁放在队列中排队阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来执行,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。
三、CAS锁