一、乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复 (读-比较-写)的操作。
Java中的乐观锁基本通过CAS操作实现,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
二、悲观锁
悲观锁是一种悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞,直到拿到锁。
Java 中的悲观锁就是synchronized
,AQS 框架下的锁则是先尝试 CAS 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 ReentrantLock
三、自旋锁
1、定义:
自旋锁即是,当线程在等待竞争锁的时候不进行上下文切换,而是保持运行状态等一等(自旋),等持有锁的线程释放锁即可立即获得锁,避免线程上下文切换的消耗。
2、最大等待时间
线程自旋任是处于运行状态的,所以会一直消耗CPU,如果一直获取不到锁,就会造成CPU资源浪费。所以需要设置一个自旋等待的最大时间。
如果线程自旋时间超过了等待的最大时间还是获取不到锁,那么线程会停止自旋进入阻塞状态。
3、优缺点
-
优点: 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗(这些操作会导致线程发生两次上下文切换)
-
缺点: 锁竞争激烈或者持有锁的线程需要长时间占用锁执行同步块,不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到cpu,造成 cpu 的浪费
4、自适应自旋
自适应意味着自旋的时间(次数)不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么JVM就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
- 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
5、实现原理
自旋锁的实现原理同样也是CAS。
四、可重入锁
可重入锁也叫递归锁。即一个线程在持有一把锁时,可以重复的获取该锁,不会因为锁已经被自己持有而等待锁的释放。可以重复的获取n次,只是在释放的时候,也需要相应的释放n次。
可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。
在Java中ReentrantLock
和 synchronized
都是可重入锁。
五、共享锁、独占锁
-
独占锁: 每次只能有一个线程持有锁。独占锁是一种悲观保守的加锁策略,当每次访问资源时都要加上互斥锁。
synchronized
就是独占锁。 -
共享锁: 允许多个线程同时获取锁,并发的访问资源。共享锁是一种乐观锁。Java中
ReadWriteLock
读写锁就是一种共享锁,它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
六、读写锁
读写锁,将锁分为读锁和写锁。在没有写锁的情况下,读是无阻塞的,在一定程度上提高了执行效率。写锁是互斥的,不允许多个线程同时获得写锁,并且读锁和写锁也是互斥的。
读读不互斥,读写互斥,写写互斥,由JVM控制。
七、公平锁、非公平锁
- 公平锁: 加锁前检查是否有排队等待的线程,优先排队等待的线程,先到先得。
- 非公平锁: 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动队尾等待
- 非公平锁性能比公平锁高5-10倍,因为公平锁需要在多核的情况下维护一个队列。
- Java中的
synchronized
是非公平锁,ReentrantLock
默认的lock()
方法采用的是非公平锁。
八、锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再由轻量级锁升级到重量级锁。锁的升级是单向的,只能从低到高升级,不会出现锁的降级。
1、重量级锁
重量级锁是依赖于操作系统的Mutex Lock。synchronized
就是重量级锁,是通过对象内部的一个叫做监视器锁(monitor)来实现的,但是监视器锁本质依赖于底层操作系统的Mutex Lock来实现的。而操作系统实现线程间的切换需要从用户态转为核心态,这个成本非常高,状态之间转换需要相对比较长的时间。这就是synchronized
效率低的原因。JDK1.6以后为了提高性能,引入了轻量级锁和偏向锁。
2、轻量级锁
轻量级锁是相对于重量级锁而言的,使用时不需要申请互斥量。而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
3、偏向锁
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起
来让这个线程得到了偏护。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所
以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
九、锁优化
-
减少锁持有时间: 只在有线程安全要求的程序上加锁。
-
减小锁力度: 将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap
。 -
锁分离: 最常见的锁分离就是读写锁
ReadWriteLock
,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue
从头部取出,从尾部放数据。 -
锁粗化: 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
-
锁消除: 锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。