目录
锁策略:
乐观锁与悲观锁:
根据接下来的锁冲突(即多个线程竞争同一个锁对象)概率的大与不大,决定接下来该怎么做
乐观锁:预测锁冲突的概率比较小
悲观锁:预测锁冲突的概率比较大
一般来说乐观锁要做的工作比较少,效率比较高,悲观锁要做的事比较多,效率比较低(根据不同情况分析)
轻量级锁与重量级锁:
轻量级锁:上锁解锁更高效
重量级锁:上锁解锁更低效
(轻量级锁和重量级锁是一对,乐观锁与悲观锁是一对,一个乐观锁也可以是轻量级锁或者是一个重量级锁)
自旋锁与挂起等到锁:
自旋锁是轻量级锁的一种典型实现;挂起等待锁是重量级锁的一种典型实现
自旋锁:抢不到锁会一直尝试获取锁,比较消耗cpu资源,但是能在锁释放的第一时间获取锁,通常是经过用户态的,不经过内核态,速度更快
挂起等待锁:抢不到锁不会一直尝试获取锁,会进行一段别的任务,在锁释放的第一时间不能获取锁,可能要过很久才能拿到锁,通常是经过内核态的,速度较慢
互斥锁与读写锁:
synchronized是互斥锁
互斥锁:就是单纯的加锁,为代码块内的所有代码加锁,没有更细化的划分,只有两步:加锁与解锁
读写锁:有对读和写的划分,分为三步:给读加锁,给写加锁,解锁
读锁与读锁之间没有锁竞争(读操作不会涉及线程不安全问题)
读锁与写锁之间有锁竞争
写锁与写作之间有锁竞争
在一写多读的情况下,读写锁比互斥锁更高效,因为互斥锁是直接全锁,读写锁内的读锁不会锁竞争
java标准库内提供了两个专门的读写锁(一个读锁,一个写锁)
可重入锁与不可重入锁:
当获取到锁的时候,再次尝试加锁如果出现死锁,该锁就是不可重入锁,如果不会出现死锁,就是可重入锁,形如:
synchronized(locker){...synchronized(locker){...}...}
当外部的synchronized获取到locker时,内部的synchronized再次尝试获取锁
如果synchronized是不可重入锁,那么内部synchronized想要获取锁需要外部的synchronized先释放锁,但外部的synchronized释放锁必须让内部的synchronized代码块执行完,这就出现了死锁情况
但庆幸的是synchronized是一个可重入锁,即内部的synchronized尝试获取锁的时候会坚持当前是否已经获取锁,如果已经获取锁了,就放行
总体来看可重入锁比较少见,在C++和python标准库中的锁以及操作系统原生的锁都是不可重入锁
公平锁与非公平锁:
获取锁的时候遵循先来后到的顺序获取锁这是公平锁,不遵循该规则就是非公平锁
synchronized是非公平锁,比如线程t1已经获取到锁,t2,t3在等待锁,t2比t3先开始等,在t1释放锁后t2,t3谁先获取锁是随机的
偏向锁:
偏向锁即非必要不加锁:
如线程t1一开始对锁对象locker上一个标记(很轻量),没有真正上锁,如果其他线程一直没有对locker尝试获取锁,那么线程t1执行完全部逻辑也不用解锁,比上锁解锁更便捷;
但如果其他线程有尝试对locker获取锁对象,那么这些线程也会先对锁对象上一个标记,这时候jvm就会通知先来的线程要真正上锁了
synchronized原理:
synchronized既是乐观锁也是悲观锁,既是轻量级锁也是重量级锁,轻量级锁部分基于自旋锁实现,重量级锁基于挂起等待锁实现;synchronized是互斥锁,是可重入锁,非公平锁
synchronized会基于当前锁的竞争情况,自适应!
即如果锁竞争激烈:就采取悲观锁与重量级锁的状态运行
如果锁竞争不激烈:就采取乐观锁与轻量级锁的状态运行
synchronized的锁升级:
因为自旋锁有忙等的特点,非常吃CPU资源,所以一旦锁竞争变大后,那么多锁都是自旋状态,非常占用CPU,所以要升级为重量级锁,这就代表这些线程要放弃CPU,由内核进行后续调度,但是主流的操作系统的调度开销是非常大的,极端情况下会非常非常大
(主流jvm只支持锁的升级,可能后续新版本jvm能支持锁的降级)
synchronized锁消除:
锁消除发生在编译阶段
编译器会在编译阶段检测当前代码是否是多线程编程即是否有必要加锁,如果没有还使用了synchronized,就会在编辑阶段自动去除synchronized,比如单线程情况下使用了StringBuffer
synchronized锁粗化:
锁的粒度:
即synchronized代码块内包含代码的数量,代码越多粒度越大,串行化覆盖率越大;代码越少,粒度越小,串行化覆盖率越低,越有利于并发
程序猿在书写代码的时候应尽量追求小粒度,提高并发的概率
假如一段代码来回加锁解锁,比如连续使用StringBuffer实例化对象进行append100W次,这就涉及到了100W次加锁解锁,此时编译器会将锁优化成一个更粗粒度的锁,即一开始上锁,append100W次完成后再解锁,这样虽然锁的粒度上升了,但是减少了上锁解锁的次数,上锁解锁开销也是很大的,而且每次上锁解锁导致的锁竞争都可能引入一定的等待开销,通过锁的粗化操作,这样在一定程度上,也能使程序效率升高。
因锁导致的死锁问题:
因为Java中的synchronized是可重入锁,所以不存在一个线程一把锁重复获取导致死锁的问题。其他导致死锁的问题主要有以下两点:
两个线程两把锁:
M个线程N把锁:
解决方案:
为所有锁获取的对象进行编号,即locker1,locker2,locker3......
让所有的线程按照统一标准顺序获取锁,比如按照从小到大的顺序获取锁或者从大到小
如:
CAS(compare and swap):
比较寄存器A和内存M中的数据值,如果相同就将寄存器B中的值写入内存M中
以一段伪代码理解CAS的逻辑:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
但实际上CAS只是CPU的一条指令,具有原子性,即不存在线程不安全问题,所以CAS即使不加锁也能保证线程安全
基于CAS可以实现很多操作:
基于CAS的原子类:
原子类:AtomicInteger,AtomicLong,AtomicDouble....其在多线程情况下是线程安全的
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
//原子类,除此之外还包括AtomicLong,AtomicDouble.....
AtomicInteger integer = new AtomicInteger(0);
Thread thread1 = new Thread(() -> {
for (int i = 0;i < 5000; i++) {
//这一步当于integer++;
//根据方法名知道是先获取再++
//该方法是基于CAS实现的,所以不存在线程不安全问题
integer.getAndIncrement();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0;i < 5000; i++) {
integer.getAndIncrement();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(integer);
}
}
上述代码结果稳定是10000,如果使用的是Integer或者基本数据类型,就会存在线程不安全问题
getAndIncrement内部逻辑:
通过伪代码的形式展示:
基于CAS的自旋锁:
以伪代码的形式展现逻辑:
一般在锁冲突概率比较小的情况下也就是乐观锁的情况下实现成自旋锁比较好
在获取锁的时候内部while()循环内为空,但并不存在内存可见性的线程不安全问题,因为CAS是一个指令级别的命令;而编译器优化导致的内存可见性问题是将都内存指令换成了读寄存器指令,CAS本身不可被编译器调整,如果调整了就不是CAS本身了,所以不存在内存可见性问题
CAS的aba问题:
以某段伪代码为例:
当然也不一定为aba也可以是abaca或者abcdefg......a
为解决这个问题通常有两种方法:
1.规定数据单向变化即只能越来越大或者只能越来越小
2. 如果需求不同意数据的单向变化,那么可以通过引入版本号单向变化这一变量作为CAS检查的标准。比如value一开始是100 version为1 之后value 变为200再变为100,此时version为3,将version作为CAS的评判标准,那么version为1的value(100)和version为3的value(100)是不同的。