1.乐观锁 and 悲观锁

什么是乐观锁?什么是悲观锁?
乐观锁/悲观锁,严格意义上不是锁,而是一种整体的策略,常用于数据库场景。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

乐观锁和悲观锁的适用场景是什么?

  1. 响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。
  2. 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大。
  3. 重试代价:如果重试代价大,建议采用悲观锁。

2.读写锁

读写锁基本介绍

  • ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。ReentranReadWriteLock是其实现类
  • 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。 每次只能有一个写线程,但是可以有多个线程并发地读数据。
  • 所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
  • 理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。

读写锁的适用场景是什么?

在一些共享资源的读和写操作,且读写操作严重不均衡的场景下可以用读写锁。

读写锁互斥原则是什么?

  • 读者——读者,能共存(即可以用多个线程同时的读)
  • 读者——写者,不能共存(即读的时候不能有其他线程去修改,或者修改的时候不能有其他线程去读)
  • 写者——写者,不能共存(即修改的时候不能再有其他线程去修改)

3.可重入锁和不可重入锁

当一个线程获得当前实例的锁lock,并且进入了方法A,该线程在方法A没有释放该锁的时候,是否可以再次进入使用该锁的方法B?
可重入锁:在方法A释放该锁之前可以再次进入方法B;如ReentrantLock和synchronized
不可重入锁:在方法A释放锁之前,不可以再次进入方法B
在java 中,synchronized和java.util.concurrent.locks.ReentrantLock是可重入锁。

4.CAS机制

什么是CAS机制?
CAS机制全称compare and swap,翻译为比较并交换,是一种有名的无锁(lock-free)算法。也是一种现代 CPU 广泛支持的CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,直接在CPU内部就完成了。

CAS的三个操作参数

  1. 内存位置M(它的值是我们想要去更新的)
  2. 预期原值E(上一次从内存中读取的值)
  3. 新值V(应该写入的新值)

CAS操作过程
首先读取内存位置M的原值,记为E,然后计算新值V,将当前内存位置M的值与E比较(compare),如果相等,则在此过程中说明没有其它线程来修改过这个值,所以把内存位置M的值更新成V(swap),当然这得在没有ABA问题的情况下(ABA问题会在后面讲到)。如果不相等,说明内存位置M上的值被其他线程修改过了,于是不更新,重新回到操作的开头再次尝试执行(自旋)。

CAS存在的问题
1.ABA问题:CAS算法实现的一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化,比如线程M从内存位置W取出值A,这时N也取出A,N操作后A变成了B,然后N将B又变回A,这时线程M在CAS操作时发现内存中仍然是A,然后M执行成功,M虽然执行成功,但实际上就出现了ABA问题。
2.CAS的循环时间长开销大问题:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。
3.CAS的只能保证一个共享变量的原子操作问题:当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。

CAS和synchronized的适用场景

  • CAS适用于写比较少的场景(多读场景,冲突一般较少)
  • synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

5.自旋锁

什么是自旋锁?
自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁不保证公平性,也无法保证可重入性。

为什么使用自旋锁?自旋锁的优点是什么?

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是活跃的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁存在的问题是什么?
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
自旋锁不保证公平性,非公平自旋锁无法满足等待时间最长的线程优先获取锁,就会存在“线程饥饿”问题。

什么是自适应自旋锁?
自适应意味着自旋的时间不再是固定的, 而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中, 那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

6.synchronized的优化

在JDK1.5中,synchronized是性能低效的。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。 这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题,从而提高程序的执行效率。导致在JDK1.6上synchronize的性能并不比Lock差。

锁清除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁。

锁粗化
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。

偏向锁
背景:虽然是多线程,但是很多时候,请求锁的往往是同一个线程。
偏向锁:当锁被第一次加锁时,会记录是被哪个线程加锁,以后这个线程再加锁/解锁操作,就不走正是通道,而走快速通道;一旦遇到其它线程也来加锁,这个快速通道就消失了。

轻量级锁
针对多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争情况下;采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。

重量级锁
重量级锁是JVM中为基础的锁实现。会阻塞、唤醒请求加锁的线程。