历经千帆, 归来仍是少年
锁策略是针对多线程和并发编程来说的, 它主要是管理多个线程访问同一共享资源, 即发生锁竞争时该怎么办, 不同的锁策略会影响性能和并发程度, 以下介绍几种锁策略
乐观锁和悲观锁并不是具体的锁, 而是两类锁, 是根据预测的锁竞争激烈程度的结果来定义乐观锁和悲观锁的
乐观锁和悲观锁属于两种不同的应对多个线程或事务对共享资源同时进行修改或者访问的方式
乐观锁: 预测多个线程或事务对共享资源同时进行修改或者访问的概率较小, 因此允许多个线程同时访问同一个共享资源, 但是在更新时会检查是否有其它线程对此资源进行过更改, 从而采取回滚或重新执行等策略悲观锁: 预测多个线程或事务对共享资源同时进行修改或者访问的概率较大, 因此只允许一个线程对共享资源进行独立访问和修改.轻量级锁和重量级锁是根据同步操作的时长和锁竞争程度
这里的同步指的是不同线程对共享资源的访问和控制
轻量级锁: 适用于短时间内的同步, 即短时间内允许多个线程同时操作共享资源. 当一个线程尝试获取轻量锁时, 如果没有竞争, 就会很轻易地获取到锁, 并且不需要阻塞其他线程, 但是如果存在竞争就会升级为重量级锁. 轻量级锁创建和销毁的开销较小. 轻量锁的设计是为了在并发情况下既可以提供足够的性能, 又能应对竞争的发生.重量级锁: 适用于长时间的同步或高度竞争, 这里的长时间指的是在高度竞争中, 锁频繁升级为重量锁, 导致较多的线程被阻塞, 上下文切换次数增加, 而造成了性能的降低. 从而确保多个线程对共享资源的访问是有序且互斥的自旋锁: 线程在尝试获取锁失败时, 不会陷入阻塞, 而是循环检测锁的状态, 期望其他线程尽快释放锁. 这种锁策略(同步策略)适用于短时间的锁竞争(否则会大量消耗CPU资源), 和轻量级锁一样. 自旋锁省去了阻塞和上下文切换的开销挂起等待锁: 线程在尝试获取锁失败时, 会立即陷入阻塞, 待锁被释放再尝试去获取锁, 适用于长时间的竞争. 是重量级锁的经典实现互斥锁: 我们之前代码中出现的锁就都属于互斥锁, 互斥锁就是一个线程对锁对象加锁, 另一个线程如果也尝试对这个锁对象加锁就会陷入阻塞, 适用于互斥访问读写锁: 读锁和读锁之间不存在锁竞争, 写锁和写锁, 读锁和写锁之间才存在锁竞争, 即允许多个线程同时拥有读锁, 但是如果有了写操作, 在写入时要独占锁, 此时其他的所有读写线程都会被阻塞, 这适用于读多写少的情况.公平锁: 在锁被释放给另一个线程加锁时, 优先给陷入阻塞时间最长的线程加锁非公平锁: 在锁被释放给另一个线程加锁时, 随机给等待锁的线程加锁.操作系统和java原生锁都是非公平锁, 在实际开发中, 想要实现公平锁, 需要加入一个队列来实现先来后到的顺序, 这需要额外的空间成本
关于可重入性在死锁那个文章中介绍过https://editor.csdn.net/md/?articleId=133634215
可重入锁: 允许同一线程连续对同一锁对象进行加锁不可重入锁: 不允许同一线程连续对同一锁对象进行加锁synchronized锁默认为乐观锁, 当线程竞争激烈时就成为悲观锁.
synchronized锁默认为轻量级锁(基于自旋锁实现), 当线程竞争激烈时就成为重量级锁(基于挂起等待锁实现)
synchronized锁是互斥锁
synchronized锁是非公平锁
synchronized锁是可重入锁
比较并修改, 将内存中的值和寄存器中的值相比较, 如果一致, 则用另一值来更新内存中的值, 这看似是两步操作, 但实际上CAS是一个原子操作. 既然是原子操作就可以用来代替上锁解决一些并发安全问题, 避免了传统上锁机制带来的性能问题.
应用CAS可以用来实现原子类和自旋锁
原子类用CAS实现, 使用原子类可以实现线程安全.
原子类有以下几种
- AtomicInteger 对int型变量的原子操作
- AtomicLong 对long型变量的原子操作
- AtomicReference 对引用类型变量的原子操作
- AtomicBoolean 对boolean型变量的原子操作
下面以之前说过的两个线程对同一变量递增操作为例:
package Thread;
class Add{
int a = 0;
public void add(){
for(int i = 0 ; i < 50000; i++){
a++;
}
}
}
public class ThreadDome11 {
public static void main(String[] args) {
Add a = new Add();
Thread t1 = new Thread(()->{
a.add();
});
Thread t2 = new Thread(()