认识并发中常见的锁
Posted hssq
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了认识并发中常见的锁相关的知识,希望对你有一定的参考价值。
文章目录
1. 锁的作用
锁是确保线程安全最常见的做法
利用锁机制对共享数据做互斥同步,这样在同一时刻,只有一个线程可以执行某个方法或者某个代码块,这样就可以保证线程安全
2. 乐观锁和悲观锁
1)乐观锁
乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下是否发生冲突,如果发生冲突则放弃操作,否则执行操作
2)悲观锁
悲观锁在操作数据时比较悲观,认为别人会同时修改数据,因此在操作数据之前先上锁,直到操作完成后释放锁,期间其他人不能修改数据
3)乐观锁和悲观锁在 Java 中的典型实现
-
悲观锁在 Java 中的应用就是通过使用 synchronized 和 Lock 加锁来进行互斥同步
-
乐观锁的一个重要功能就是检测出数据是否发生访问冲突,一般使用以下两种方法实现此功能:
- 引入数据版本号
- CAS机制
4)数据版本机制
为每段数据添加一个版本号,线程从主存中读取到数据时会将数据版本号一并读出,在对数据进行修改完成之后,会将自身数据的版本号 +1,在提交到主存之前先对比自身数据版本和主存数据版本,当满足 提交的数据版本大于当前主存中的数据版本时才能执行数据更新,否则就说明发生了冲突,认为此次操作失败
3. CAS 机制
1)什么是 CAS
CAS 全称 Compare and swap,字面意思就是:比较并交换
CAS 包括三个操作数:内存中的原数据 V,旧的预期值 A,需要修改的新值 B
具体操作如下:
- 比较 A 与 V 是否相等(比较)
- 如果比较相等,将 B 写入 V(交换)
- 返回操作是否成功
当多个线程同时对某资源进行 CAS 操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号
2)CAS 的 ABA 问题
什么是 ABA 问题
假设存在两个线程 t1 和 t2,有一个共享变量 num,初始值为1
线程 t1 想把1变成2,但在这之前,线程 t2 将 1 变成 2,再从 2 变回 1
到 t1 执行操作时,CAS 判断原数据等于预期值,就认为没有被修改过,所以 t1 会继续后边的操作
ABA 问题引来的 BUG
当数据类型为基本数据类型时,那么此时对结果不会有影响
当数据类型是一个引用类型时,那么就可能会产生影响,因为其他线程可能更改了引用的对象中的东西,但是引用还是那个引用。就比如:我的手机被别人借去用了几天,又还了回来,手机还是那个手机,但里面的东西可能就和之前不一样了
ABA 问题的解决方法
在 CAS 机制中加入数据版本机制,给要修改的值加上数据版本号,在 CAS 比较当前值和旧值是否相等的同时,还要比较数据版本是否相同
4. 读写锁
读写锁中拥有两把锁,一个读锁,一个写锁,在执行加锁操作时需要额外表明需要读锁还是写锁。
特点:
- 同一时刻允许多个持有读锁的线程对共享资源进行读操作
- 同一时刻只允许一个持有写锁的线程对共享资源进行写操作
- 当当前线程持有共享资源的读锁时,同一时刻其他持有写锁的线程会被阻塞
读写锁更适合于 “ 频繁读,不频繁写 ” 的场景中
1)Java 标准库中提供的读写锁
Java 标准库中提供了 ReentrantReadWriteLock 类,来实现读写锁
- ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个类提供了 lock / unlock 方法进行加解锁
- ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个类提供了 lock / unlock 方法进行加解锁
5. 偏向锁、轻量级锁和重量级锁
1)偏向锁
偏向锁不是真正的 “ 加锁 ”,只是给对象头中做了一个标记,记录这个锁属于哪个线程,如果后续没有其他线程来竞争锁,那么就不用进行同步操作了,避免了加锁解锁的开销
2)轻量级锁
在锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他锁会通过自旋的方式尝试获取锁,不会阻塞,性能提高
3)重量级锁
在锁是轻量级锁的时候,另一个线程虽然自旋,但自旋不会一直持续下去,当自旋一定次数还没有获取到锁,就会进入阻塞,轻量级锁就会膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
6. 自旋锁
按之前的方式,线程在抢锁失败后会进入阻塞状态,放弃 CPU,需要过很久才能再次被调度
实际上,大部分情况下,虽然抢锁失败,但是过不了多久,锁就会被释放,没必要放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题
工作原理:
如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极端的时间内到来
一旦锁被其他线程释放,就能在第一时间获取到锁
7. 公平锁和非公平锁
假设有 A、B、C 三个线程,A 先尝试加锁,加锁成功,然后 B 尝试加锁,加锁失败,阻塞等待;然后 C 尝试加锁,加锁失败,阻塞等待
当 A 释放锁之后,谁先获取到锁呢?
**公平锁:**遵守 “ 先来后到 ” 的原则,B 比 C 先来,A 释放锁之后,B 就能先于 C 获取到锁
**非公平锁:**不遵守 “ 先来后到 ” 的原则,B 和 C 都有可能获取到锁
一张简图让你了解公平锁和非公平锁
注意:
- 操作系统内部的线程调度是随机的,如果不做任何限制,锁就是非公平锁,如果要实现公平锁,就需要额外的数据结构,来记录县城们的先后顺序
- 公平锁和非公平锁没有好坏之分,关键看使用场景
Java线程并发中常见的锁
随着互联网的蓬勃发展,越来越多的互联网企业面临着用户量膨胀而带来的并发安全问题。本文着重介绍了在java并发中常见的几种锁机制。
1.偏向锁
偏向锁是JDK1.6提出来的一种锁优化的机制。其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式。在JVM中使用-XX:+UseBiasedLocking
package jvmProject; import java.util.List; import java.util.Vector; public class Biased { public static List<Integer> numberList = new Vector<Integer>(); public static void main(String[] args) { long begin = System.currentTimeMillis(); int count = 0; int startnum = 0; while(count<10000000){ numberList.add(startnum); startnum+=2; count++; } long end = System.currentTimeMillis(); System.out.println(end-begin); } }
初始化一个Vector,往里面添加10000000个Integer对象,然后输出时间差。以此来测试偏向锁的性能。至于为什么要使用Vector而不使用ArrayList呢?
因为ArrayList是线程不安全的,Vector是线程安全的。这样说可能还不够具体,可以翻看一下源码吧。
Vector中的几乎所有操作是带有sychronized的,而ArrayList是没有的,所以Vector是线程安全的。
接下来我们来测试一下,开启偏向锁和不开启偏向锁对程序性能的影响有多大。
配置JVM启动(开启偏向锁)参数为:
配置JVM启动(关闭偏向锁)参数为:
Perfect!开启偏向锁的程序运行时间明显较短,开启偏向锁比不开启偏向锁,在单个线程中操作一个对象的同步方法,是有一定的优势的。其实也可以这样理解,当只有一个线程操作带有同步方法的Vector对象的时候,此时对Vector的操作就转变成了对ArrayList的操作。
偏向锁在锁竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难保持在偏向模式,此时,使用偏向锁不仅得不到性能的优化,反而有可能降低系统的性能,因此,在激烈竞争的场合,可以尝试使用
-XX:-UseBiastedLocking参数禁用偏向锁。
2.轻量级锁
如果偏向锁失败,Java虚拟机就会让线程申请轻量级锁,轻量级锁在虚拟机内部,使用一个成为BasicObjectLock的对象实现的,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈帧中。在BasicLock对象内部还维护着displaced_header字段,用于备份对象头部的Mark Word.
当一个线程持有一个对象的锁的时候,对象头部Mark Word信息如下
[ptr |00] locked
末尾的两位比特为00,整个Mark Word为指向BasicLock对象的指针。由于BasicObjectLock对象在线程栈中,因此该指针必然指向持有该锁的线程栈空间。当需要判断一个线程是否持有该对象时,只需要简单地判断对象头的指针是否在当前线程的栈地址范围即可。同时,BasicLock对象的displaced_header,备份了原对象的Mark word内容,BasicObjectLock对象的obj字段则指向持有锁的对象头部。
3.重量级锁
当轻量级锁失败,虚拟机就会使用重量级锁。在使用重量级锁的时,对象的Mark Word如下:
[ptr |10] monitor
重量级锁在操作过程中,线程可能会被操作系统层面挂起,如果是这样,线程间的切换和调用成本就会大大提高。
4.自旋锁
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。
在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。
以上是关于认识并发中常见的锁的主要内容,如果未能解决你的问题,请参考以下文章