目录
锁策略
锁策略不是Java语言独有的一门技术,任何和锁有关的话题,都会涉及到锁策略。
策略:解决问题的办法。
乐观锁和悲观锁
锁的实现者预测锁被阻塞的概率大不大,来根据这个冲突的概率来决定接下来咋做。
两个线程针对同一个对象加锁,就会造成阻塞。
通常来说,乐观锁做的事情相对少一些。效率也比悲观锁要高一些。
悲观锁做的事情就比较多,所有效率也就没有乐观锁高。
但是都不绝对。
乐观锁
造成锁冲突的概率并不大
假设数据一般情况下并不会发生并发冲突,所以只有在数据提交更新的时候才会检测是否产生冲突,如果发现冲突了,则返回用户错误的信息,让用户决定如何做。
悲观锁
冲突的概率很大
假设每次都是最坏的情况,在拿数据的时候,都会认为别人会修改数据,所有在每次拿数据的时候都会进行加锁操作,这样别人想拿数据就得阻塞等待。直到解锁。
轻量级锁和重量级锁
轻量级锁
加锁解锁的过程更快更高效。
一个乐观锁可能是一个轻量级锁 但是不绝对
重量级锁
加锁解锁的过程更慢更低效。
一个悲观锁可能是一个重量级锁 但是不绝对
自旋锁和挂起等待锁
自旋锁是轻量级锁的一种典型实现
挂起等待锁是重量级锁的一种典型实现
自旋锁
如果获取锁失败,就会立即尝试重新获取锁,一直无限循环,直到获取到锁为止。一旦其他线程释放锁,那么就能第一时间获取到锁。
通过是用户态操作,不需要经过系统内核,时间相对较短
挂起等待锁
不会一直无限循环等待锁的释放,这就导致挂起等待锁就不能第一时间获取到锁。
那么通过上面的几种锁策略,我们的synchronized这把锁是属于那种呢?
我们的synchronized即使乐观锁,也悲观锁,即使轻量级锁,也是重量级锁。
轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现。
synchronized会根据我们当前锁的竞争激烈程度,自适用来运行。
如果锁的竞争不激烈,那么就是乐观锁,轻量级锁的状态运行。
如果锁的竞争很激烈,那么就是悲观锁,重量级锁的状态运行。
读写锁和互斥锁
互斥锁
synchronized是互斥锁 只有两个操作 就是单纯的加锁和解锁
进入synchronized修饰的代码块:加锁
退出synchronized修饰的代码块:解锁
读写锁
给读和写两种锁分开:
读写锁有三种操作:
1:给读加锁
2:给写加锁
3:解锁
如果多个线程读取同一个变量,就不会产生线程安全问题
读写锁中约定:
1:读锁和读锁之间不会产生竞争 不会产生阻塞等待
2:写锁和写锁之间会有锁竞争
3:读锁和写锁之间也有锁竞争
相当于把加锁操作区分的更加精细了。
可重入锁和不可重入锁
如果一个锁,在一个线程中,连续对该锁加锁两次,不死锁,就是可重入锁,如果死锁了,就是不可重入锁。
但是synchronized是可重入锁,因为加锁的时候会判定下,看当前尝试申请锁的线程是不是已经是锁的拥有者了,如果是则直接放行。
CAS
compare and swap 比较并交换
和多线程密切相关的一个东西。
可以让咱们不加锁也能实现线程安全的操作。
寄存器A的值和内存M的值进行比较,如果值相等,就把寄存器A的值和内存M的值进行交换。
交换相当于赋值操作。
CAS操作是CPU的一条指令,这一条指令已经是CPU的最小单位
这一条指令就能完成上述比较并交换的功能。
基于CAS实现原子类
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0); //原子类 AtomicInteger
//保证++ -- 的时候线程安全
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//num++
num.getAndIncrement();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//num++
num.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//get获取数值
System.out.println(num.get());
}
上述代码我们可以看见,还是同时两个线程一起执行,但是这个两个线程进行++的操作是通过AtomicInteger来实现的。
这个类就是基于CAS实现的,可以让我们不加锁就能实现多线程环境下的安全问题。
CAS在自增之前,会检查内存和寄存器中的值是否一样,如果一样则自增,如果不一样,则更新内存中的值,在进行自增操作。
CAS本身就是CPU的最小单位,已经无法再次拆分了,所有上述的交换,和比较操作是无法再次拆分的。
实现自旋锁
反复检查锁的状态是否解开了,如果发现锁的状态已经解锁,就能第一时间拿到锁。
owner用来记录当前锁被那个线程持有
unlock操作就是解锁 直接置为null
如果当前owner为空,比较就成功,就把当前线程的引用赋值到owner中,加锁完成,循环结束。
如果比较不成功,意味着owner非空,表示已经有线程把锁持有了,此时CAS就啥都不干,直接返回false,循环继续。
此时这个循环就会转的飞快,会不听的尝试循环这个锁是否被释放,一旦锁释放,就立即能获取到。但是CPU在盲等。
实现自旋锁,目的就是为了盲等,就是为了能够最快速度拿到锁。