博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java~今日学习各种锁策略(乐观锁 悲观锁 读写锁等等)、CAS机制和synchronize的原理及其优化机制(锁消除 偏向锁 自旋锁 膨胀锁 锁粗化)
阅读量:4050 次
发布时间:2019-05-25

本文共 5235 字,大约阅读时间需要 17 分钟。

文章目录

锁策略

乐观锁 VS 悲观锁

  • 乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。

乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

读写锁

  • 把加锁操作分成了俩种 一是读锁二是写锁 也就是说在读和读之间是没有互斥的 但是在读写和写写之间就会存在互斥
  • 如果一个场景是一写多度 那么使用这个效率就会很高

重量级锁 VS 轻量级锁

  • 首先我们要知道加锁有一个很重要的特性就是保证原子性 原子性的功能其实来源于硬件(硬件提供了相关的原子操作的指令, 操作系统把这些指令统一封装成一个原子操作的接口, 应用程序才能使用这样的操作)
  • 所以在加锁过程中 如果整个加锁逻辑都是依赖于操作系统内核 那此时就是重量级锁(代码在内核中的开销会很大) 如果大多数操作都是用户自己完成的 少数由操作系统内核完成 这种就是轻量级锁

挂起等待锁 VS 自旋锁

  • 挂起等待锁表示当前获取锁失败后, 对应的线程就要在内核中挂起等待 (放弃CPU进入等待队列) 需要在锁对象释放之后由操作系统唤醒 (通常都是重量级锁)
  • 自旋锁表示当前获取锁失败后 不是立刻放弃CPU 而是快速频繁的再次访问锁的持有状态, 一旦锁对象被释放就能立刻获取到锁(通常都是轻量级锁)

自旋锁的效率更高, 但是会浪费一些CPU资源 (自旋相当于CPU在那空转)

公平锁 VS 非公平锁

  • 这种情况就是如果已经有多个线程在等待一把锁的释放 当释放之后, 恰好又来了一个新的线程也要获取锁
  • 公平锁: 保证之前先来的线程优先获取锁
  • 非公平锁: 新来的线程直接获取到锁, 之前的线程还得接着等待

实现公平锁就需要付出一些额外的代价 所以公平锁的效率是略低于非公平锁的

可重入锁

  • 一个线程针对同一把锁连续加锁俩次, 不会死锁, 这种就是可重入锁

可重入锁这就像是大门的三保险锁一样 我锁一层再锁一层 这种并不会造成我们死锁住自己 因为当我们想出去的时候又可以一层一层的开锁

死锁

  • 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
  • 我们常说的死锁有三个经典场景
  1. 一个线程一把锁 连续加锁俩次才 (保证使用的不是可重入锁)
  2. 俩个线程, 俩把锁, 相互获取对方的锁
  3. n个线程, n把锁, 哲学家就餐问题
  • 死锁产生的四个必要条件:(较为理论简单了解)

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

CAS

  • CAS的全称是 compare and swap(字面意思就是比较交换) 他是基于硬件提供的一种基础指令, 也是基于这样的指令, 就可以实现一些特殊的功能(实现锁)
  • 针对不同的操作系统,JVM 用到了不同的 CAS 实现原理
  • 简而言之,是因为硬件予以了支持,软件层面才能做到。

我们假设内存中的原数据val,旧的预期值new,需要修改的新值tmp。

  1. 比较 new 与 val 是否相等。(比较)
  2. 如果比较相等,将 tmp 写入 val。(交换)
  3. 返回操作是否成功。
  • 可见当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

CAS的使用

在这里插入图片描述

上图所示就是使用的CAS封装了一些原子类如下面代码示例第一个使用CSA的锁第二个不使用 显然结果第一个是线程安全的第二个线程是不安全的

import java.util.concurrent.atomic.AtomicInteger;/** * Created with IntelliJ IDEA. * Description: If you don't work hard, you will a loser. * User: Listen-Y. * Date: 2020-08-04 * Time: 20:40 */public class Demo2 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(); Thread thread = new Thread() {
@Override public void run() {
for (int i = 0; i < 5000; i++) {
atomicInteger.addAndGet(1); } } }; Thread thread1 = new Thread() {
@Override public void run() {
for (int i = 0; i < 5000; i++) {
atomicInteger.addAndGet(1); } } }; thread.start(); thread1.start(); thread.join(); thread1.join(); System.out.println(atomicInteger.get()); }}
/** * Created with IntelliJ IDEA. * Description: If you don't work hard, you will a loser. * User: Listen-Y. * Date: 2020-08-04 * Time: 20:53 */public class Demo3 {
private static int count = 0; public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override public void run() {
for (int i = 0; i < 5000; i++) {
count++; } } }; Thread thread1 = new Thread() {
@Override public void run() {
for (int i = 0; i < 5000; i++) {
count++; } } }; thread.start(); thread1.start(); thread.join(); thread1.join(); System.out.println(count); }}

CAS的缺陷 ABA问题

  • 这个问题就是加入现在有个num为0 有一个线程把他修改为1, 然后紧接着又有一个线程把他修改为0了 那此时仅仅通过CAS的比较是无法区分的
  • 解决这个问题就需要引入额外的信息 (给变量加一个版本号 每次进行修改 都递增版本号)

synchronize的原理

  • synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性,原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行;可见性就是当一个线程在修改共享数据时,其他线程能够看到,保证可见性,volatile关键字也有这个功能;有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。

以synchronize为例学习锁优化

编辑器和JVM配合进行的

锁消除

  • 锁消除本质是以编辑器和JVM代码运行的情况智能的判断当前的锁有没有必要加 如果没有必要, 就会直接把锁干掉
/** * Created with IntelliJ IDEA. * Description: If you don't work hard, you will a loser. * User: Listen-Y. * Date: 2020-08-04 * Time: 21:21 */public class Demo4 {
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer(); buffer.append("listen"); buffer.append("listen"); buffer.append("listen"); buffer.append("listen"); System.out.println(buffer); }}

在这里插入图片描述

  • 到库中我们可以发现StringBuffer是加锁线程安全的 但是在我们上面写的代码中完全不用考虑线程安全问题 所以在实际运行的时候就把锁消除了

偏向锁

  • 第一个尝试加锁的线程 不会真正的加锁 而是进入偏向锁(一种很轻量的锁) 知道其他线程也来竞争这把锁的时候 才会取消偏向锁的状态 真正的进行加锁
  • 这个很像我去球馆打球的时候借用人家球馆里的球的时候 当人多有人和我竞争的时候我就得去花钱租 人少的时候我就可以登记一下直接玩
  • 总而言之上述这俩个优化机制就是能不加锁就不加锁

自旋锁

  • 当有很多线程竞争锁的时候, 偏向锁状态被消除 此时没有得到锁的线程并不会直接直接挂起放弃 而是使用自旋锁 的方式来尝试去再次获取锁

  • 自旋锁能保证让其他想竞争锁的线程尽快得到锁 但是也相应付出了一定的cpu资源

  • 还是上面我去球馆打球的例子 如果此时就一个人来和我竞争这个篮球 我不会立马放弃 而是会稍微等会 看我是不是快回家了

  • 没有上锁就是无所状态, 在使用syn上锁的时候, 没有发生竞争就是偏向锁, 如果有少数线程发生了竞争就使用cas乐观乐观的自旋锁不断的在访问获取锁状态也就是轻量级锁,当线程访问到达十次还不能获得锁就会进入重量级锁

锁膨胀

  • 当锁竞争更加激烈的时候 此时就会从自旋状态膨胀成重量级锁(挂起等待锁)
  • 还是我去球馆打球 竞争太激烈的时候 等待的人就会回家了 不玩了

锁粗化

  • 如果一段路基中 需要多次加锁 解锁 并且在解锁的时候没有其他线程来竞争 此时就会把多组的锁操作合并在一起 (合并后的锁的粒度很比较粗 所以叫锁粗化)
  • 还是我去打球的例子 比如我正在玩突然想去卫生间 然后此时还没有人和我竞争这个篮球 我就没必要把他放回去 上完卫生间再去拿回来 我直接抱着篮球上卫生间多省事

转载地址:http://cjsci.baihongyu.com/

你可能感兴趣的文章
CImg库编译使用.
查看>>
openstack虚拟机创建流程
查看>>
openstack网络总结
查看>>
excel 查找一个表的数据在另一个表中是否存在
查看>>
centos 7 上配置dnsmasq 同时支持ipv4和ipv6的DHCP服务
查看>>
AsyncTask、View.post(Runnable)、ViewTreeObserver三种方式总结frame animation自动启动
查看>>
Android中AsyncTask的简单用法
查看>>
概念区别
查看>>
final 的作用
查看>>
在Idea中使用Eclipse编译器
查看>>
Idea下安装Lombok插件
查看>>
zookeeper
查看>>
Idea导入的工程看不到src等代码
查看>>
技术栈
查看>>
Jenkins中shell-script执行报错sh: line 2: npm: command not found
查看>>
8.X版本的node打包时,gulp命令报错 require.extensions.hasownproperty
查看>>
Jenkins 启动命令
查看>>
Maven项目版本继承 – 我必须指定父版本?
查看>>
通过C++反射实现C++与任意脚本(lua、js等)的交互(二)
查看>>
利用清华镜像站解决pip超时问题
查看>>