文章目录
- 前言
- 一、悲观锁
- 二、乐观锁
- 三、自旋锁
- 原理
- 自旋锁优缺点
- 优点
- 缺点
- 自旋锁时间阈值(1.6 引入了适应性自旋锁)
- 自旋锁的开启
- 四、可重入锁(递归锁)
- 五、读写锁
- 六、公平锁
- 七、非公平锁
- 八、共享锁
- 九、独占锁
- 十、轻量级锁
- 十一、重量级锁
- 十二、偏向锁
- 十三、分段锁
- 十四、互斥锁
- 十五、同步锁
- 十六、死锁
- 十七、锁粗化
- 十八、锁消除
- 十九、synchronized
- 二十、Lock和synchronized的区别
- 二十一、ReentrantLock 和synchronized的区别
- 总结
前言
一、悲观锁
举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
-
synchronized关键字和Lock的实现类都是悲观锁
-
适用场景:适合写操作多的场景,先加锁可以保证写操作时数据正确。
二、乐观锁
举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。你看乐观锁就是天生乐观!
认为自己在使用数据时不会有别的线程修改数据或资源
,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等
- 乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。
- 适用场景:适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
三、自旋锁
原理
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需自旋,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋需消耗 cup 的,如果一直获取不到锁,则线程长时间占用CPU自旋,需要设定一个自旋等待最大事件在最大等待时间内仍未获得锁就会停止自旋进入阻塞状态。
自旋锁优缺点
优点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗(这些操作会导致线程发生两次上下文切换)
缺点
锁竞争激烈或者持有锁的线程需要长时间占用锁执行同步块,不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费
自旋锁时间阈值(1.6 引入了适应性自旋锁)
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理
自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能
JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的
在 1.6 引入了适应性自旋锁,自旋的时间不固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间
自旋锁的开启
JDK1.6 中-XX:+UseSpinning 开;XX:PreBlockSpin=10 为自旋次数
JDK1.7 后,去掉此参数,由 jvm 控制
四、可重入锁(递归锁)
可重入锁(递归锁),指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。
敲黑板:可重入锁的一个好处是可一定程度避免死锁。
以 synchronized 为例,看一下下面的代码:
上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。
五、读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制
如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥由 jvm 控制的,程序员只需要上好相应的锁
要求代码只读数据,可以很多人同时读,但不能同时写,可上读锁代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock
, 也 有 具 体 的 实 现ReentrantReadWriteLock
。
六、公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。
七、非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)
在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
八、共享锁
共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。
在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。
九、独占锁
独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。
JDK中的synchronized
和java.util.concurrent(JUC)
包中Lock
的实现类就是独占锁。
十、轻量级锁
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。
十一、重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。
这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。
十二、偏向锁
Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
十三、分段锁
分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。
ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。
线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
十四、互斥锁
互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。
互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
十五、同步锁
同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
Java中的同步锁: synchronized
十六、死锁
死锁是一种现象:如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。
Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。
所以一定要注意程序的并发场景,避免造成死锁。
十七、锁粗化
锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。
举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。
经过锁粗化后就变成下面这个样子了:
十八、锁消除
锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。
举个例子让大家更好理解。
上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。
test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。
我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除。
十九、synchronized
synchronized是Java中的关键字:用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。
-
作用于实例方法时,锁住的是对象的实例(this);
-
当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁, 会锁所有调用该方法的线程;
-
synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。
二十、Lock和synchronized的区别
-
Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
-
Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
-
Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
-
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
-
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
-
Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
-
-
synchronized的优势:
-
足够清晰简单,只需要基础的同步功能时,用synchronized。
-
Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
-
使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。
-
二十一、ReentrantLock 和synchronized的区别
ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
-
划重点
-
相同点:
-
主要解决共享变量如何安全访问的问题
-
都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
-
保证了线程安全的两大特性:可见性、原子性。
-
-
-
不同点:
-
ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
-
ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
-
ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
-
ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
-
ReentrantLock 通过 Condition 可以绑定多个条件
-
总结
前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下: