一、乐观锁 VS 悲观锁
1. 乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。 乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
2. 悲观锁:即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。Java里面的同步原语synchronized关键字的实现也是悲观锁。。
二、读写锁 VS 一把大锁
1. 一把大锁: 在多线程情况下,我们可以想象一个模型。此时我有一个账本,多个读者和写者站在这个账本前,无论是哪种角色,必须保证每次只有一个人来操作这个账本。为了保证线程安全,那么可以通过一把大锁,无论是谁操作这个账本,都会加锁。
- 缺点:实际上多个读者去操作账本,并不影响安全性。100W读者情况下,明明可以同时操作账本,只有一把大锁的时候却限制为只能一个人操作账本,其他人苦等,大大降低了效率。
2.读写锁:多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。读写锁(readers-writer lock),根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能;在执行加锁操作时需要额外表明读写意图
- 复数读者之间并不互斥
- 而写者则要求与任何人互斥。
- 读锁之前也会去检查一下写锁,如果打开就可以使用读锁,必须等写完成。
三、用户态自旋锁 VS 内核态重量级锁
1. 内核态锁:当线程A拥有了锁之后,线程B如果此时想要获得A的锁,则需要等待进入阻塞态,等待线程A 释放锁之后,然后从阻塞态进入唤醒态去获得A释放的锁;然后线程的阻塞和唤醒在操作系统层面需要从用户态切换到核心态,这样是非常消耗资源的工作,需要选择一种优化来优化这种弊端;并且我们会发现,很多的对象锁的锁定时间并不是非常长,如整数的自加的操作,这些操作的代价即很短的时间内进行阻塞和唤醒线程的操作显然是很浪费的;
- 把当前线程的状态修改成没有抢CPU资格的状态(BL0CK)
- 把线程的id加入到一个队列中
- 放弃CPU,灵活性太差
lockVar是一个锁的标志,0是开,1是上锁
lock() {
if (lockVar == 0) { //保证为原子操作
lockVar = 1;
return;
}
}
2. 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 cup不干活,空转一会。如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗cup 的,说白了就是让cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
优点:
- 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
缺点:
- 如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup 的线程又不能获取到cpu,造成cpu 的浪费。所以这种情况下我们要关闭自旋锁;
四、可重入锁
1. 可重入锁:
即允许同一个线程多次获取同一把锁,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。synchronized关键字锁都是可重入的。
- Class a = new A ();
- a.m1;
第一次抢锁是调用m1,第二次抢锁是调用m2的时候,实际上是抢了两次锁,享受了可重入锁的福利
Class A {
synchronized m1() { //第一次抢锁
m2();
}
synchronized m2 {
…………
}
}
原理:
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
五、公平锁 VS 不公平锁
1. 公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。如果不是必要的情况下,不要使⽤公平锁公平锁会来带⼀些性能的消耗的
2.不公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。非公平锁性能比公平锁高5~10 倍,因为公平锁需要在多核的情况下维护一个队列。Java 中的synchronized 是非公平锁,ReentrantLock 默认的lock()方法采用的是非公平锁