多线程之常见的锁策略
目录:
一、乐观锁 VS 悲观锁
二、互斥锁 VS 读写锁
三、轻量级锁 VS 重量级锁
四、自旋锁 VS 挂起等待锁
五、公平锁 VS 非公平锁
六、可重入锁 VS 不可重入锁
一、乐观锁 VS 悲观锁
站在锁冲突概率的角度来看,冲突较少,锁竞争不激烈,此时为乐观锁,反之,锁竞争很激烈则升级为悲观锁。
对于乐观锁来说,假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何做。
对于悲观锁来说,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
这两种思路不能说谁优谁劣, 而是看当前的场景是否合适。
synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突,我们可以引入一个 "版本号" 来解决,只要访问一次,版本号就会增加一次,所以版本号是唯一的,可以用来校验。
二、互斥锁 VS 读写锁
关于互斥锁,就是像synchronized这样的锁,提供加锁和解锁两个操作。如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。
关于读写锁,提供了三种操作:针对读加锁、针对写加锁、解锁。因为多线程针对同一个变量进行并发读,这个时候没有线程安全问题的,也不需要加锁控制。读锁和读锁之间,没有互斥;写锁和写锁之间,存在互斥;写锁和读锁之间,存在互斥。
在代码中,如果只是读操作,加读锁即可,如果有写操作,加写锁。
假设如果当前有一组线程都去读(加读锁),这些线程之间没有锁竞争的,也没有线程安全问题。
假设如果当前的一组操作有读也有写,才会产生锁竞争。
在很多开发场景中,读操作非常高频,比写操作的频率高很多,读写锁特别适合于 "频繁读, 不频繁写" 的场景中。synchronized 不是读写锁。
三、轻量级锁 VS 重量级锁
站在加锁操作的开销角度来看,轻量级锁的开销较小,重量级锁的开销较大。
重量级锁的加锁机制严重依赖了操作系统,很容易造成线程的调度,造成大量的内核态和用户态切换(内核态可以想象成银行柜台的工作人员,用户态可以想象成顾客,办理一个业务,频繁的切换工作人员和顾客,这样到最后办理这样一个业务的开销会很大)。而轻量级锁的加锁机制是尽量不会依赖操作系统去切换内核态和用户态。
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁。
四、自旋锁 VS 挂起等待锁
关于自旋锁线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU。这个时候就可以使用自旋锁来处理这样的问题,一直循环等待。
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止。第一次获取锁失败, 第二次的尝试会
在极短的时间内到来。一旦锁被其他线程释放, 就能第一时间获取到锁。
关于挂起等待锁,一旦抢锁失败后就放弃 CPU了,过了一会儿再次获取锁,就算抢锁成功了,此时已经“沧海桑田”了,举个例子:
当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了!
挂起等待锁: 陷入沉沦不能自拔,过了很久很久之后, 突然女神发来消息, "咱俩要不试试?" (注意,
这个很长的时间间隔里, 女神可能已经换了好几个男票了)。
自旋锁: 死皮赖脸坚韧不拔,仍然每天持续的和女神说早安晚安。 一旦女神和上一任分手, 那么就能
立刻抓住机会上位。
自旋锁是一种典型的 轻量级锁 的实现方式:
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁。
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源。 (而挂起等待的时候是
不消耗 CPU 的)。
synchronized 中的轻量级锁策略可能就是通过自旋锁的方式实现的。
五、公平锁 VS 非公平锁
此处的公平可以理解为“先来后到”的原则,举个例子:
女神正在和舔dog1处对象,但是此时正在排队的有:dog2舔了一年,dog3舔了半年,dog4舔了一个月,dog5舔了一周。当女神和dog1分手后,那么dog2就上位扶正,依次类推。这就是公平锁。
反之,当女神和dog1分手后,dog2、dog3、dog4、dog5随机上位,每个人的机会是公平的,但是这对于舔了很久的dog们来说是不公平的,这就是非公平锁。
操作系统内部的线程调度就可以视为是随机的。 如果不做任何额外的限制, 锁就是非公平锁。如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序。
公平锁和非公平锁没有好坏之分, 关键还是看适用场景。
synchronized 是非公平锁。
六、可重入锁 VS 不可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入
锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的。
或者
不可重入锁:一个线程针对一把锁连续加锁两次,出现死锁。
可重入锁:一个线程针对一把锁,连续加锁多次都不会死锁。