本篇主要介绍各种锁相关的概念,没有过多详细的讲解,帮助大家一起梳理整个java锁知识体系。
关于锁的概念,很多都是从不同角度去划分的,有些是互斥的概念,如悲观锁和乐观锁,有些是兼容的概念,如悲观锁可以是公平锁,可以是阻塞锁,而有些则纯粹是锁实现的算法,如CAS算法是乐观锁的一种实现方式。
1、悲观锁和乐观锁
悲观锁:总是往坏处想,认为当前线程使用的共享资源一定会被其他线程修改,所以需要在开始使用时直接上锁,结束使用时才会释放锁,这样同一时间只能有一个线程能占用共享资源,其他线程进入阻塞状态或者失败重试。
java中的synchronized和ReentrantLock都是属于悲观锁。
乐观锁:总是往好处想,认为当前使用的共享资源不会被其他线程修改,但是当自己需要修改的时候会判断在此期间有没有其他线程修改过,如果没有就修改成功,否则需要重试。
java.util.concurrent.atomic包下的原子变量类就是属于乐观锁,使用的是一种CAS的算法。
2、CAS算法和ABA问题
CAS(Compare And Swap),是一种乐观锁实现算法,涉及三个参数:当前内存值V,预期值A,期望值B,首先程序从内存读取V,然后比较V是否等于A,如果等于,则更新为B,否则失败重试,其中比较更新的过程是一个基于CPU指令的硬件层面的原子操作。
ABA问题,是属于CAS算法的一个缺点,上述CAS的过程中,比较更新是原子操作,但是读取内存V到比较的期间不是原子的,如果线程1读取到V,然后线程2读取V,并将值从A改为B然后再改回A,那么对于线程1来说,进行比较更新的时候发现V仍然是A,可以更新成功,但是对于线程1来说,这个值其实已经发生过改变,只是它不知道。因此乐观锁使用CAS算法的时候就会引入一个版本号的概念,这样即便有线程将A最终修改为A,但是版本也变了,能被其他线程识别到。
3、共享锁和排他锁
共享锁:也叫读锁、S锁,运行多个线程共享同一把锁,用于读操作;
排他锁:也叫互斥锁、独占锁、写锁、X锁,同一时间只能允许一个线程使用锁,用于写操作;
4、公平锁和非公平锁
公平锁:各个线程根据请求获取锁的时间顺序来依此获得锁,线程的排队可以用FIFO队列实现;
非公平锁:允许插队,各个线程抢占获取锁;
java中synchronized就是非公平锁,ReentrantLock和ReentrantReadWriteLock默认都是非公平锁,可以通过构造器传true,创建公平锁。
5、阻塞锁、非阻塞锁、自旋锁和自适应自旋锁
阻塞锁:线程获取锁失败会进入休眠状态,直到被唤醒后继续尝试获取锁;
非阻塞锁:线程获取锁失败不会挂起,直接返回失败;
自旋锁:也是非阻塞锁,线程获取锁失败后不会休眠,而是通过循环抢占锁,直到获取锁为止,简答说就是个for循环,由于这种是要耗费cpu资源的,所以一般不会无限循环获取,而是会尝试一定次数后失败;
自适应自旋锁:自旋+阻塞,上面说自旋锁会耗费cpu,因此需要指定循环次数避免浪费资源,而自适应自旋锁,不是固定的循环次数,会根据当前线程获取锁的历史经验来判断有没有必要自旋、需要自旋多少次以及可能需要阻塞。
如果业务逻辑简单,锁的占用时间很短,那么可以用自旋锁,性能会很高,相反,如果因为逻辑复杂导致锁占用时间长,那么自旋只会浪费cpu资源,因此这时候让线程挂起是很好的选择。
6、偏向锁、轻量级锁和重量级锁
偏向锁:多数情况下,只有一个线程使用共享资源,为了提升效率,锁占用后会在对象头中记录当前线程id和偏向锁标识,如果一直是这个线程访问资源就避免了重复的加锁解锁,直到出现竞争线程后升级到轻量级锁,其他线程通过自旋等待锁。
轻量级锁:所有线程获取锁的过程不会进入阻塞状态,通过自旋抢占锁,这种程序响应时间快,但是很耗费cpu资源;
重量级锁:线程获取不到锁就进入阻塞状态,直到锁被释放后线程被唤醒,程序响应时间慢。
7、可重入锁
可重入锁:也叫递归锁,允许同一个线程多次占用同一把锁,一般都是方法内调用方法,或者递归调用自身方法多次占用锁。
java中synchronized和Reentrant开头的锁都是可重入锁。