当前位置:网站首页 > 技术博客 > 正文

juc java并发




参考

  • 狂神说Java_JUC并发编程最新版通俗易懂
  • 什么是乐观锁什么是悲观锁

就是工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。

image-20210321110312209

image-20210321115140571



进程是程序的一次执行过程,包含进程控制块、程序段、数据三部分

1️⃣ 动态性

  • 动态性是进程最基本的特征,表现为:由创建而产生,由调度而执行,得不到资源而暂停执行,由撤销而消亡;有一定的生命周期
  • 程序只是一组有序的指令集合

2️⃣ 并发性

  • 引入进程的目的就是和其他进程能并发执行
  • 程序不能并发执行

3️⃣ 独立性

  • 进程实体是一个能独立运行的基本单位,是系统中独立获得资源和独立调度的基本单位
  • 程序不能作为一个独立的单位进行运行

不行,Java是通过native本地方法调底层C++写的方法,Java无法直接操作硬件

  • 并发:多个事件在同一时间间隔内发生(cpu一核,模拟出来多条线程快速交替运行)
  • 并行:多个事件在同一时刻发生(cpu多核,多个线程可以同时执行)

查看cpu的核数

image-20210321111429010

image-20210321111527079

image-20210321111556525

NEW 尚未启动的线程处于此状态 RUNNABLE 在Java虚拟机中执行的线程处于此状态 BLOCKED 被阻塞等待监视器锁定的线程处于此状态(IO操作,wait,juc锁定) WAITING 正在等待另一个线程执行特定动作的线程处于此状态(sleep,join) TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态(sleep,join) TERMINATED 已退出的线程处于此状态。
  • sleep不释放锁,wait释放锁
  • 来自不同的类:sleep()函数在Thread类中,wait()函数属于Object类
  • 使用范围不同:sleep可以在任何地方使用,wait只能使用在同步代码块中

可重入,就是可以重复获取相同的锁而不会出现死锁;synchronized和ReentrantLock都是可重入的

 

image-20210321153436838

 

image-20210321153415399

真正的开发中,线程只是一个资源类(包含属性),没有任何附属的操作

模拟卖票

  • 以前我们会将类继承Runnable接口
  • 现在会将作为一个资源类,里面不添加关于线程的操作
 
     

官方文档地址:https://tool.oschina.net/apidocs/apidoc?api=jdk-zh

image-20210321133621186
使用方法:创建锁、加锁、业务代码、解锁
image-20210321134513971

我们接下来用的方式来解决上述买票问题,最常用的实现类就是可重入锁

  • 可以看到看到有两个构造方法,可以指定使用 公平锁/非公平锁
  • 公平锁:十分公平,先来后到
  • 非公平锁:不公平,可以插队

image-20210321133926406
修改类

 
       
  • 是内置的关键字,是一个Java类
  • 无法判断锁的状态,可以判断是否获取到了锁
  • 会自动释放锁,需要手动释放锁(如果不释放锁,会造成死锁)
  • 假如有两个线程:线程1、线程2;线程1获得了锁

    :如果线程1阻塞了,线程2就会一直等待,造成死锁

    :如果线程1阻塞了,线程2不会一直等待,可以通过方法尝试获取锁

  • 两者都是可重入锁,但是不可中断,为非公平锁;而锁可判断锁状态,并且可以设置为公平锁/非公平锁
  • 适合锁少量代码同步代码,适合锁大量代码同步代码


生产者消费者——线程之间的通信问题

  • 通过实现,我们常用 +
  • 通过怎么实现呢,用 +

image-20210321143716982

两个线程A、B实现:

如果number不等于0,则number-1;如果number等于0,则number+1

 

结果:
image-20210321141752317

虚假唤醒问题

如果再加两个线程呢?

 

看结果,会出现2、3的情况;这就是因为if判断只判断一次,两个线程可能同时+1;造成了虚假唤醒的问题
image-20210321142530487
image-20210321142942399
修改代码:判断改成判断


 
         

image-20210321143908744
修改代码:

 
          

image-20210321145522108
根据结果,成功实现!但是发现一个问题,线程的执行都是随机的,怎么进行有序的实现呢?A=>B=>C=>D

Condition精准通知唤醒

可以设置多个condition监视器,每个监视器监视一个线程,精确等待唤醒某个线程

 
          

结果:
image-20210321151156051



如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁(对象、 Class)

创建两个线程A、B,A线程执行发短信方法,B线程执行打电话方法,谁先执行?

 

结果:先发短信再打电话
image-20210321190231935

那如果让发短信休眠2s呢?

 

结果:还是先发短信再打电话
image-20210321190405716
这是为什么呢?并不是因为A先执行,而是有锁的存在

  • 锁的对象是方法的调用者,也就是对象,因此打电话和发短信方法锁的是同一个对象,谁先拿到锁谁就先执行

如果新增一个普通方法hello,那先是hello还是发短信呢?

 

image-20210321193511631
根据结果,先执行hello,这是因为是一个普通方法,没有锁,不受锁的影响

如果有两个对象,是先发短信还是先打电话

 

两个对象,两个调用者,所以有两把锁,所以按时间顺序执行

image-20210321194003558

修改两个方法为静态方法,只有一个对象,是先打电话还是发短信

 

根据结果,先发短信
image-20210321194256128
因为static静态方法在类加载的时候就有了,因此这里锁的是Class模板,也就是,全局唯一;也就是两个方法拿的仍是同一把锁

那如果两个方法为静态方法,有两个对象,是先打电话还是发短信?

 

根据结果,仍是先发短信
image-20210321194617283
因为锁的是,也就是说两个方法仍是同一把锁

如果是一个普通的同步方法和一个静态的同步方法,只有一个对象

 

结果:
image-20210322095928191
一个锁的是模板,一个锁的是对象,因此两个方法不是一把锁,因此按时间顺序运行

如果是一个普通的同步方法和一个静态的同步方法,有两个对象

 

同样两个方法不是一把锁,因此按时间顺序运行
image-20210322100003462



image-20210322100452113

ArrayList不安全

并发情况下,不安全,我们通过一个简单的案例来测试:

 


根据结果,发现报错了(并发修改异常)

那么怎么解决呢?

  • 方案一:换成线程安全的
     
  • 方案二:利用Collections工具类
     
  • 方案三:利用JUC包中的类
     

引入CopyOnWriteArrayList

image-20210322141229600

  • 简称,是计算机程序设计领域的一种优化策略
  • 多个线程调用的时候,读取的时候固定,但写入时会复制,避免写入造成的数据覆盖问题

其效率比更高,因为Vector在方法上都用了关键字,会降低效率

而是用锁实现的,底层也是数组实现,不过添加的时候会先拷贝一份新的数组,最后再拷贝回去
image-20210322134138150
image-20210322134213815


HashSet不安全

并发情况下,不安全,我们通过一个简单的案例来测试:

 
               

同样,:并发修改异常
image-20210322134948437
那么怎么解决呢?

  • 方案一:利用Collections工具类
     
  • 方案三:利用JUC包中的类
     

引入CopyOnWriteArraySet

image-20210322141215322

  • 简称,是计算机程序设计领域的一种优化策略
  • 多个线程调用的时候,读取的时候固定,但写入时会复制,避免写入造成的数据覆盖问题

同样是用锁实现的,底层也是数组实现,不过添加的时候会先拷贝一份新的数组,最后再拷贝回去
image-20210322135648975
image-20210322135621456


HashMap不安全

并发情况下,不安全,我们通过一个简单的案例来测试:

 
                

同样,:并发修改异常
image-20210322140807339
那么怎么解决呢?

  • 方案一:利用Collections工具类
     
  • 方案三:利用JUC包中的类
     

引入ConcurrentHashMap

image-20210322141145100
融合了和二者的优势

但是hashtable每次同步执行的时候都要锁住整个结构。看下图:

image-20210414145603675
ConcurrentHashMap正是为了解决这个问题而诞生的,其锁的方式是稍微细粒度的,引入了的概念;

  • 可以理解为把一个大的Map拆分成N(默认为16)个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
  • 在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中:

通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全;原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的



image-20210322144924183

image-20210323075320880

image-20210323075243997

  • 可以有返回值
  • 可以抛出异常
  • 重写call()方法而不是run()

实现Runnable接口时,我们通过进行启动,因为的构造方法可以传入对象
image-20210323080136708
那么怎么实现Callable呢?我们无法直接通过进行启动,但是我们可以通过间接的启动
image-20210323080306487
查看帮助文档,可以看到接口有一个启动类,我们点进去看看
image-20210323080439782
可以看到,它有两个构造方法,分别可以传入和对象,这就将两者联系了起来
image-20210323081206052
于事我们就可以通过来启动接口的实现类







 

image-20210323080953589
注意

  • 通过的get()获取call方法的返回值,该方法可能会产生阻塞(可能返回结果需要大量的计算,很耗时),一般情况下将其放在最后一行或者使用异步通信来处理
  • 任务多线程并发访问时为啥只会被执行一次
    image-20210323081830058


就是一个减法计数器

image-20210323082713513

image-20210323083302699

 

如果不加
image-20210323083117066
加了之后
image-20210323083141410


相当于加法计数器

image-20210323083357327

image-20210323083929351

 
                    

计数信号量

image-20210323084317870

  • ,获得许可正,如果许可证已经满了,等待其他线程释放许可证
  • ,释放许可证

作用:多个共享资源互斥使用,并发限流,控制最大的线程数

 

image-20210323084704901



image-20210325230129179
代码测试:定义一个缓存区用于读写操作,然后启动5个线程分别进行读和写,测试

  • 首先测试不加锁的情况下
     

    image-20210325233019529
    根据结果,可以看到写入时被插队,这是不允许的!

  • 加读写锁,实现只能同时有一个线程写,多个线程读

    也就是写锁为(一次只能被一个线程占有),读锁为(多个线程可以同时占有)

    • 读-读:可共存
    • 读-写:不可共存
    • 写-写:不可共存
     

    image-20210325233255584
    根据结果,我们实现了写入时不能被插队,但是读取可以多个线程读取



image-20210325234511948

关系图:

image-20210325235643917

队列阻塞

image-20210325235738373
方式 抛出异常 有返回值、不抛出异常 阻塞等待 限时等待 添加 add() offer() put() offer( , ) 移除 remove() poll() take() poll( , ) 判断队列首 element() peek()

抛出异常(add、remove、element)

 

image-20210409190803875

 

image-20210409190850740

 

image-20210409191247343

不跑出异常(offer、poll、pick)

 

等待阻塞(put、take)

 

限时等待()

 

image-20210409200508319

一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行 ,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的 是尝试添加到队列中的首个已排队插入线程的元素;如果没有这样的已排队线程,则没有可用于移除的元素并且 将会返回 。对于其他 方法(例如 ), 作为一个空 collection。此队列不允许 元素。

 
                          

池化技术:程序的运行就会占用系统资源就会占用系统资源,为了优化资源的使用,就引入了池化技术,事先准备好一些资源,需要使用就来取,用完即放回;例如 线程池、连接池、内存池、对象池

线程池的好处:线程复用、可以控制最大并发数、管理线程

  • 降低资源消耗
  • 提高响应速度
  • 方便管理

如何创建线程池呢?中提供了类

image-20210412140530673
其中有一些静态方法用于创建线程池
image-20210412140902077

newSingleThreadExecutor

 

image-20210409214330526

newFixedThreadPool

 

image-20210409220213831

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小

 

image-20210409220319946

查看、、的源码,可以发现本质上就是创建了一个ThreadPoolExcutor对象

image-20210409232955804

image-20210409233027397
image-20210409233052778
再查看的源码,可以看到七大参数

 

各种参数的含义好比银行办理业务,是已经开放的服务窗口,是候客区,假设人流量非常大,就需要多开放几个服务窗口,就是最大开放的服务窗口数;再假如很少有人办理业务,过了一定的时间就关闭窗口,就是要关闭窗口的事件;拒绝策略就好比银行满了,再来人就不让进了

可以看到的核心线程池大小设置未0,最大线程池大小设置为,约等于21亿;也就是说通过Executors.newCachedThreadPool()创建的线程池可以支持并发的线程数介于0~21亿之间,这是十分耗费资源的

因此,阿里巴巴官方手册有以下规定:
image-20210409233957716

image-20210410002637368
image-20210410002432207
我们来自定义一个线程池,拒绝策略为

 

image-20210410001714211
如果i<=9,超过了最大承载,则会抛出异常
image-20210410001755571
修改拒绝策略为:可以看到没有抛出异常,而是由main线程处理


 

image-20210410002758331
修改拒绝策略为:可以看到不抛出异常不执行

 

image-20210410002912020
策略同类似:不抛出异常不执行,但是会尝试竞争

CPU密集型

电脑的cpu是几核就定义为几,定义为常数换台电脑就不行了

 

IO密集型

大于 判断程序中耗费IO的线程 即可



新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算

函数式接口(Functional Interface)是jdk8引入的,有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。并且这类接口使用了进行注解,函数式接口可以被隐式转换为 lambda 表达式

JDK 1.8 之前已有的函数式接口:

  • java.lang.Runnable
     
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

JDK 1.8 新增加的函数接口:

  • java.util.function

    image-20210410111741105

这个package中的接口大致分为了以下四类:

  • Function:接收参数,并返回结果,主要方法
  • Consumer:接收参数,无返回结果, 主要方法为
     
  • Supplier:不接收参数,但返回结构,主要方法为
  • Predicate:接收参数,x返回boolean值,主要方法为

函数式接口:有参数且需要返回值

 

实现一个Function接口

 

image-20210410113447834

判断型接口:有参数,返回值为布尔型

 

实现一个Predicate接口

 

image-20210410125206650

消费性接口:没有返回值,有参数

 

实现Consumer接口

 

image-20210410125950617

供给型接口:无参数,指定返回值类型

 

实现Supplier接口

 

image-20210410130339781



大数据时代分为存储+计算,存储交给数据库、集合等来处理,计算就交给流来做

Java中就提供了用于流式计算
image-20210410133603700

 
                                 

ForkJoin出现于jdk1.7,用于并行执行任务 ,提高效率,用于大数据量的情况(分支合并)

大数据:Map Reduce——将大任务拆分成小任务

image-20210410194839704

工作窃取:里面维护的都是双端队列
image-20210410195206428

我们可以在JUC中找到类
image-20210410204610944
其中有一个方法,可用于执行一个
image-20210410204632095
image-20210410204736063
我们需要返回值,查看,可以找到方法进行计算
image-20210410205524323





1️⃣ 编写任务

 

2️⃣ 测试比较

 

image-20210410214523018



异步回调通常用
image-20210411132654111
发起两个异步请求,一个有返回结果,一个没有返回结果

 

image-20210411134850651
如果有返回结果的异步回调报错,就会走失败回调的方法,返回233
image-20210411135011585



是 JVM 提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

怎么保证可见性?就需要和JMM挂钩

Java内存模型,不存在的东西,是一种概念一种约定

关于JMM同步的约定

  • 线程解锁前,必须立刻把自己的共享变量刷回主存
  • 线程加锁前,必须读取主存中的最新值到工作内存中
  • 加锁和解锁是同一把锁

线程分为:工作内存、主内存

image-20210411135950221

可以保证可见性,不能保证原子性,可以避免指令重排的现象

1. 保证可见性

 

image-20210411141806904

开启一个线程,当num=0时不停的死循环;然后让主线程休眠1s后修改num=1,也就是将主内存中的num修改为1;看到结果程序并没有停止,这是因为t线程并没有拿到主内存中num最新的值,不知套其发生了变化,也就是t线程对main线程的变化不可见

如何解决呢?只需要通过关键字修饰num即可保证其的可见性

image-20210411142303690

可以看到,程序立马停止了

2. 不保证原子性

什么是原子性?也就是一个线程执行的时候不能被打扰分割,要么同时成功要么同时失败

 

image-20210411144731153
根据结果,发现并不保证原子性的操作,为什么不安全呢?

我们反编译看看,可以看到一行代码在底层是多行操作,因此不能保证原子性,所以是不安全的

 

那如果不通过Lock和Synchronized 怎么保证原子性呢?

可以通过包中的原子类解决原子问题 ,这些类的底层都直接和操作系统挂钩,在内存中修改值,这些类是特殊的存在
image-20210411150212298

 

image-20210411154037289

3. 禁止指令重排

指令重排:计算机并不是按照我们编写的程序去执行

源代码–》编译器优化代码重排–》指令并行重排–》内存系统重排–》执行

处理器在指令重排的时候,会考虑数据之间的依赖性

 

指令重排可能会导致一些错误的结果,如下图所示:

image-20210411155952539

使用可以避免指令重排,底层实现是通过 内存屏障 实现的,可以保证特定的操作执行顺序,也可以保证某些变量的内存可见性

image-20210411160615817

在单例模式中使用的最多

 

优点: static变量会在类装载时初始化,不涉及多个线程访问该对象的问题,可以省略synchronized关键字

缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getinstance(),甚至永远没有调用,则会造成资源浪费!

 

优点: 延迟加载,真正用的时候才实例化对象,提高了资源的利用率

缺点:存在并发访问的问题,以下测试并发访问情况

 

image-20210411162651485
根据结果,可以看到有5个线程打印了结果,也就说进行了5次初始化,这是非常大的漏洞,出现了并发访问的问题

为了解决懒汉式并发访问的问题,加入了关键字

 

image-20210411164551307
根据打印结果,解决了并发访问的问题;但是这样仍然会存在问题,因为我们对象时并不是一个完整的原子性操作,而是分为以下三部:

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

单个线程A执行的情况下可以123按顺序执行,也可能由于指令重排按132执行;但是如果线程A按132顺序执行到3时来了一个线程B,此时该对象已经指向了分配的空间,因此B判断对象不是null,就会直接返回对象,但其实对象并没有进行初始化,就造成了错误

因此指令重排也会导致错误,因此完整的还加入了关键字来避免指令重排,完整代码如下:

 
 
 

image-20210411233747573
根据结果,看到创建了两个实例,也就是单例模式被破坏,那么怎么解决呢?

可以在私有构造中加锁

 

image-20210411234013067
根据结果,可以看到避免了单例模式的破坏?可是上述两个对象一个是通过单例获取,一个通过反射获取;

那如果两个对象都是通过反射获取呢?

 

image-20210411234133836
根据结果,可以看到单例模式又被破坏了,创建了两个对象!这种情况如何解决呢?

可以通过红绿灯方法实现,定义一个标志位记录对象是否创建

 

image-20210411234412838
可以看到我们通过设置标志位再次解决了这个问题,但是一旦被获取了这个关键字,单例模式仍然可以通过反射被激活成功教程,如下所示

 

image-20210411234824886
可以看到单例模式再次被破坏;因此为了让程序更加安全,通常对关键字进行加密处理

那么到底如何完全的避免反射破坏单例模式呢?我们查看的源码
image-20210411235142204
可以看到,如果是枚举类型的话,就不能通过反射获取枚举;

因此引入了第5种单例模式

 

我们再次通过反射创建对象,根据结果报错没有EnumSingle的空构造方法,这不是我们希望看到的
image-20210412000059691
我们对的class文件进行反编译,可以看到明明有空构造方法
image-20210412000353611
但是执行明明报错没有无参构造,我们使用更专业的反编译工具对class文件再进行反编译
image-20210412000629008
可以看到枚举类本质上就是继承了类,本身就是一个Class,而且没有无参构造,而是含两个参数的有参构造,我们修改代码在测试





 

image-20210412000837225
这才正确显示了报错的信息:无法反射地创建枚举对象



是 compareAndSet 的缩写:比较并交换,是CPU的并发原语

 

image-20210412002500909
我们再来看看 atomicInteger.getAndIncrement() 方法是怎么实现的?我们该方法的源码
image-20210412002756242
可以看到是由调用了方法,而就是类的一个实例


什么是类

  • Java无法操作内存,只能通过调用C++来操作内存,就是Java通过C++操作内存的接口
  • 就类似于Java通过native关键字来调用C++本地方法来和操作系统交互

image-20210412004045789
可以看到,底层是一个do while循环,也就是一个自旋锁

因此:就是比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操作;如果不是,就一直循环,因为底层是一个do while循环(自旋锁)

CAS有三个操作数:

  • 期望的值
  • 比较的值
  • 更新的值

缺点

  • 底层是自旋锁,循环耗时
  • 一次性只能保证一个共享变量的原子性
  • 会存在ABA问题

比如有两个线程A,B同时向修改A的内容,但是B线程执行速度快,首先cas(1,3)将A修改为3,然后又执行cas(3,1)将A修改为1,这之后线程A再cas(1,2)将A修改为2,但此时A=1已经不是原来的1了;

这就是ABA问题

image-20210412005138299

我们来个代码模拟以下

 

image-20210412005623931
可以看到三个结果都为true,但不是我们期望的,我们希望知道谁动过A的值

可以通过类似乐观锁的方案来解决,使用 原子引用类/(带时间)
image-20210412010508978
我们使用测试以下

 

image-20210412011555412
可以看到A成功察觉到了B修改过数据,所以执行失败;和乐观锁原理相同

注意:如果泛型是包装类,注意对象引用问题(正常业务都是对象,这里是使用包装类Integer进行测试)

image-20210412011922276
如果我们的范围不再-128~127,则会失败
image-20210412012249646



悲观锁(Pessimistic Lock)

1️⃣ 简介

​ 当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】

​ 悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

img

之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

  1. 传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  2. Java 里面的同步 synchronized 关键字的实现。

2️⃣ 分类

悲观锁主要分为 和

  • 【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 【exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

3️⃣ 说明

​ 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

乐观锁(Optimistic Locking)

1️⃣ 简介

​ 乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。
img
​ 乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。

2️⃣ 实现

  1. CAS实现:Java中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式
  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功

3️⃣ 说明

​ 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

  • 公平锁:非常公平,不能插队,线程的执行必须先来后到
  • 非公平锁:非常不公平,可以插队,默认都为非公平锁!(比如一个线程3s执行完,一个线程1min执行完,如果使用公平锁严重影响某个线程的效率)
    image-20210321133926406

可重入锁(递归锁)
image-20210412012855770

代码示例:synchronized版

image-20210412013050474

执行结果:

image-20210412013256865

代码示例:Lock版

image-20210412013522089

不断的尝试,直到成功为止!

image-20210412014256436

我们来编写一个自旋锁

 

然后编写一段测试代码

 

image-20210412082802954
根据结果,总是线程解锁后,线程才能解锁;因为如果线程不解锁,就会卡住在while循环不停的尝试cas直到thread=null为止

什么是死锁?

是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象

image-20210412083125750

简单的死锁案例

 

image-20210412084034340
根据运行结果,可以看到程序卡死,因为发生了死锁,因为T1和T2分别持有lockA和lockB,但又都试图获取对方的锁!

死锁问题排查

  1. 使用命令定位进程号

    image-20210412084332447

  2. 使用查看指定进程的堆栈信息,找到死锁问题

    image-20210412084622766

    可以看到,控制台清晰的打印了找到死锁,并可以看到产生的原因就是和互相尝试获取对方的锁


















  • 上一篇: jmap使用详解
  • 下一篇: csdn积分能换钱吗
  • 版权声明


    相关文章:

  • jmap使用详解2025-08-31 07:01:01
  • 线程池的主要处理流程2025-08-31 07:01:01
  • win10鼠标光标自定义2025-08-31 07:01:01
  • dbcp object created2025-08-31 07:01:01
  • 游戏编程入门教程2025-08-31 07:01:01
  • csdn积分能换钱吗2025-08-31 07:01:01
  • 深度优先遍历怎么遍历2025-08-31 07:01:01
  • usb接口的驱动怎么安装2025-08-31 07:01:01
  • leaf spine网络架构2025-08-31 07:01:01
  • unity图形引擎开发2025-08-31 07:01:01