基本的锁策略
1.乐观锁和悲观锁
乐观锁:总是假设好的情况,认为多线程对数据进行更新的时候不会发生冲突,所以不上锁,直接进行操作,在操作过程中,通过“版本号”进行冲突检测,如果没有发生冲突就正常更新数据,如果发生冲突,就需要重新读取数据并重试操作 或者返回错误信息,让用户重新决定如何去做。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
当线程冲突严重时,就需要加锁,来避免线程频繁访问共享数据失效带来的CPU空转问题。它可以确保数据的正确性,但是会导致性能下降。
当线程冲突不严重的时候,可以采用乐观锁策略来避免多次的加锁解锁操作。它可以提高并发性能,但是需要处理冲突和重试操作,实现方式之一就是CAS。
2.读写锁:
多个线程对数据进行读操作并不会引起冲突
多个线程对数据进行写操作就可能引起冲突
一个线程进行读操作一个进行写操作可能引起冲突
因此,我们将一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
读写锁可以提高并发性,适合于读操作频繁的情况。
3.重量级锁和轻量级锁、
锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
1.CPU 提供了 "原子操作指令".
2.操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
3.JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
线程的阻塞是一件“很伤”的事情,一旦线程因为阻塞而陷入挂起等待(因为涉及到内核态,系统啥时候处理你难以揣摩),就不知道啥时候才会被唤醒,影响效率
用户态就像是你去银行用ATM取钱,自己掌握事情发展的进度,内核态就像是你去办理一件复杂的业务需要工作人员来协调处理,需要消耗大量的资源,并且这个时候这个工作人员还有可能被领导拉去谈话,上厕所等等,你只能眼巴巴干等。
重量级锁:
需要操作系统和硬件支持,涉及到大量用户态到内核态的切换,并且可能涉及到线程的切换,线程获取重量级锁失败进入阻塞状态。(mutex锁)
轻量级锁:
尽量在用户态执行操作,线程不阻塞,不会进行状态切换。
一个悲观锁很可能是一个重量级锁,一个乐观锁很可能是一个轻量级锁。
4.自旋锁和挂起等待锁
自旋锁:轻量级锁的一种典型实现,在用户态通过自旋的方式实现加锁(通过while循环不断地尝试获取获取锁)。线程获取锁失败并不会让出CPU,线程也不阻塞,不会从用户态切换到内核态,线程在CPU上空跑,当锁被释放,此时这个线程很快就会获取到锁。
挂起等待锁:通过内核态,借助系统提供的锁机制,在出现锁冲突时涉及到线程的调用,使出现冲突的线程阻塞等待(挂起)。
自旋锁可以第一时间获得锁,不会进入阻塞状态,可以提高效率,但是如果一直拿不到锁,就会让cpu空转,浪费资源。
5.公平锁和非公平锁
公平锁:遵循先来后到原则,获取锁存在优先级
不公平锁:获取锁时各凭本事,公平竞争,不遵循先来后到原则。
6.可重入锁
synchronized(锁1) {
synchronized(锁1) {
}
}
可重入锁在嵌套加锁(同一把锁)时不会产生死锁(如示例代码),不可重入锁在嵌套加锁时会产生死锁。
synchronized时可重入锁
**synchronized的锁策略
1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. synchronized的轻量级锁基于自旋锁来实现,重量级锁基于系统的互斥锁来实现
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁
加锁时编译器产生的优化
1.锁消除
锁消除是一种编译器优化技术,它通过静态分析和代码转换来消除不必要的锁操作,从而提高程序的性能。在Java中,锁操作是比较耗时的操作,因为它需要进行用户态和内核态之间的切换、上下文保存和恢复等操作。如果锁操作不是必须的,那么就会浪费大量的系统资源,影响程序的性能。锁消除就是通过静态分析和代码转换来判断锁操作是否必须,如果不必须就将锁操作删除。
比如,在单线程状态下,系统会自动取消StringBuffer的加锁机制。
2.锁粗化
加锁是一件费时费力的事情,锁粗化就是通过扩大锁的范围来减少锁操作的次数。例如,在某些情况下,程序会对同一个对象进行多次加锁和解锁操作,这会导致大量的锁操作。如果编译器发现这种情况,就可以将多个连续的锁操作合并成一个大的锁操作,从而减少锁操作的次数,提高程序的性能。
举例:
for(很多次) {
加锁{
}
加锁{
}
//此处省略个好多个加锁操作
}
这时系统就会扩大加锁的范围来减少加锁操作
加锁{
for( ){
}
}