【JavaEE】多线程常见的锁策略

简介: 哈喽,大家好~我是保护小周ღ,本期为大家带来的是多线程开发中为了保证线程安全而设计锁策略,synchronized 锁——1. 既是乐观锁,也是悲观锁2. 既是轻量级锁,也是重量级锁3. 轻量级锁是基于自旋锁实现,重量级锁是基于挂起等待锁实现4. 不是读写锁,是互斥锁5. 是可重入锁6. 是非公平锁,确定不来看看嘛~更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘

 

image.gif编辑

哈喽,大家好~我是保护小周ღ,本期为大家带来的是多线程开发中为了保证线程安全而设计锁策略, synchronized 锁—— 1. 既是乐观锁,也是悲观锁 2. 既是轻量级锁,也是重量级锁 3. 轻量级锁是基于自旋锁实现,重量级锁是基于挂起等待锁实现 4. 不是读写锁,是互斥锁 5. 是可重入锁 6. 是非公平锁,确定不来看看嘛~更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘

image.gif编辑


一、锁策略

说到锁,Java 里面常用的锁有 synchronized ,锁的意义在于在多线程并发执行的时候保证线程安全,防止出现 BUG, 造成线程不安全的原因本质上是因为线程的调度是无序的,Java本身不负责调度,是由操作系统按照一定的规则来调度线程,CPU 并发执行,但是由于其他应用程序无法干预系统线程的调度,所以可以认为任务线程的调度是无序的。

锁策略就是设计一种解决线程安全问题的办法,在程序设计中根据业务需求需要去实现一个锁,基本上就是参考常见的锁策略。


1.1 乐观锁 和 悲观锁

顾名思义:

乐观锁,用来预测接下来的锁冲突概率不大,所谓锁冲突就是多线程针对同一对象加锁,同一时间内只有一个线程可以获取到锁并执行,其他线程阻塞等待锁释放。

悲观锁,用来预测接下来的锁冲突概率很大,这就涉及到程序需要不断的加锁、解锁,所以通常来讲悲观锁纯纯的打工仔,乐观锁工作的机会是比较少的,但是也不绝对。

悲观者总是喜欢将一件事往最坏的情况下预测,并且已经做好了最坏的打算,所以啊即使当那一刻真的来临,可能也会庆幸意料之中吧。

乐观者也许是珍惜当下的心态,每一天开开心心的就好了,即使遇到很不好的事情也能很快释然吧


1.2 轻量级锁 和 重量级锁

轻量级锁,即:加锁解锁的过程更快更高效。

常常使用在不同的线程交替的执行同步块中的代码(同步代码块指在代码块前加上 synchronized关键字的代码块),这种情况下,用重量级锁是没必要的。轻量级锁所适应的场景是线程交替执行同步块的场合,锁冲突较少的情况,如果出现多线程同时竞争同一把锁,锁冲突严重,那么就会导致轻量级锁膨胀为重量级锁。锁的强度对线程的状态的影响也是不同的。

重量级锁,即 :加锁解锁的过程更慢更低效

运用的场景那必然是多线程同时竞争同一对象锁,锁冲突严重,其他线程阻塞等待锁释放,当对象锁释放的时候,因锁竞争而阻塞等待的线程就会被唤醒,继续竞争锁,没有抢到的线程又会继续阻塞等待,如此以往,对资源还是有一定的消耗的。

线程阻塞等待即:该线程暂时不参与系统的调度,也就不会被 CPU 执行了。


1.3 自旋锁 和 挂起等待锁

轻量级锁基于自旋锁的一种实现,当多线程对同一对象锁进行竞争的时候,就会造成锁冲突,但是如果这种冲突并不严重的话,比如两个线程竞争,不是你就是我的,那没竞争到锁的另一个线程此时不会 “阻塞等待 ”,而是不断的循环尝试获取锁,当然这也会浪费CPU 的执行资源(也就是忙等状态),但是当锁释放的时候,自旋等待的线程能够第一时间获取到锁

重量级锁是基于挂起等待锁的一种实现,也就是锁冲突比较严重的状态,例如:有10 个线程竞争同一把对象锁,同一单位时间内只有一个线程能拿到锁并执行,那如果 10 个线程都自旋等待那就是得不偿失的,CPU 还得是并发的执行他们,让他们一直保持随时获取锁的状态,这消耗是得不偿失的,所以当锁冲突比较严重的时候直接让等待获取锁状态的线程进入阻塞等待,暂时不参与CPU的调度执行, 虽然会有唤醒线程,带来的开销,但是相较于一直并发执行这些线程,就大大降低了CPU 的开销。CPU 每秒钟可执行几十亿条指令。

image.gif编辑

基准速度:1 GHz 等于 一秒10 亿条指令。


针对以上三种锁策略: synchronized 这把锁具有那些属性呢?

synchronized 既是悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁,轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现。

synchronized 会根据当前锁竞争的程度,自适应的转化锁属性,如果锁冲突不激烈,是以乐观锁或乐观锁的状态运行,如果锁冲突激烈,以悲观锁或重量级锁的状态运行。


1.4 互斥锁 和 读写锁

互斥锁:

在多线程编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。  

多线程对同一对象(synchronized 修饰的代码块)进行操作就会发生锁竞争,竞争同一把对象锁,那么同一单位时间内只有一个线程能拿到锁并执行,此时其他线程就 “阻塞等待”,线程进入 synchronized 修饰的代码块就意味着获取到锁了,然后加锁,出代码块释放锁,解锁。实现一种互斥访问,不是你执行就是我执行,不管谁先进入,其他人都不能继续执行了。

读写锁:

顾名思义就是对读、写操作加锁。

在线程的环境下如果多个线程同时读数据不会有线程安全问题,如果多线程允许同时对一个变量的值进行修改,那完了,你改我也改(例如:对同一变量进行自增操作,两个线程同时读取的数据是一样的,但是如果写的时候线程1 先调度执行,修改完毕,写回内存,然后线程2 调度执行,修改完毕写回内存,那线程 2 修改后的值就会把线程 1 的值覆盖掉)很可能就对数据的有效性造成重大的影响。

所以读写锁的属性:

1. 读锁和读锁之间不会产生锁竞争

2. 写锁和写锁之间有锁竞争,同一单位时间内只有一个线程能拿到锁并执行写操作

3. 读锁和写锁之间也会有锁竞争,同一单位时间内只有一个线程能拿到锁并执行读操作,然后执行写操作

Java 标准库中提供了两个专门的读写锁,读写锁是使用 ReentrantReadWriteLock 类来实现的

    • ReentrantReadWriteLock.ReadLock 表示读锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。
    • ReentrantReadWriteLock.WriteLock 表示写锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。

    1.5 可重入锁 和 不可重入锁

    如果一个线程执行 synchronized 修饰的代码块,意味着该线程获取到了对象锁,其他线程因无法获取对象锁而进入阻塞等待,等待锁释放,此时,如果该线程又执行到了 synchronized 修饰的代码块 ,是两个代码块嵌套的情况,而且使用的是同一个对象锁,概念是,一个对象只有一把锁,那么线程刚进入  synchronized 的代码块就获取了这把锁,此时又遇到需要获取该对象锁的情况,那么线程就有两种情况:

    1. 骂骂咧咧的说谁把锁抢走了,气死我了,进入阻塞等待(死锁),因为谁拿到锁,就由谁释放锁,线程进入 synchronized 修饰的代码块获取锁,出 synchronized 修饰的代码块就会释放锁,这个时候别的线程就可以获取锁,现在这种情况是线程手里拿着锁,还没释放锁,又遇到需要获取该对象锁的情况,那他就会阻塞等待锁释放,产生了死锁问题,这就叫做 不可重入锁。

    2. 由于线程手上有该对象锁,所以再遇到需要获取该对象锁的情况就直接验证一下是否是该锁的拥有者,如果是则允许进入,不会产生死锁问题,这就是是可重入锁。

    Object locker = new Object(); // 使用第三方对象锁
    synchronized(locker) {  //线程获取 locker 对象锁
       //线程再次获取 locker 对象锁,此时锁已经被线程获取了还未释放,就会有两种结果
       synchronized(locker) {
       }
    }

    image.gif

    概念有些晦涩难懂,接下来给大家举一些例子来讲述:

    不知道大家有有没有在日常生活中偶尔遇到找某件东西怎么都找不到,找了一圈又一圈最后发现要找的东西就在自己手里,害。

    image.gif编辑


    在单线程的情况下,在某些特定的情况下可重入锁就非常好使,不会产生死锁问题。

    但是如果是多个线程多把锁的情况下,即使是可重入锁,也会死锁~

    image.gif编辑

    综上所述:可重入锁只有在单线程的状态下才好用,多线程就需要注意加锁顺序了。

    image.gif编辑

    死锁的四个必要条件:

    1. 互斥使用,一个线程拿到一把锁之后,另一个线程阻塞等待锁释放(张三上厕所,李四等待)

    2. 不可抢占,一个线程拿到锁,只能自己主动释放,不能被其他线程强行占有。

    3. 请求和保存状态,张三手里拿着对象锁,还一直尝试获取锁。

    4. 循环等待,“家里的要是锁车里了,车钥匙锁家里了”,获取锁的逻辑是循环的,无法破局。


    1.6 公平锁 和 非公平锁

    因为同一单位时间内只有一个线程能获取到锁并执行,其他竞争同一对象锁的线程进入阻塞等待的状态,此时如果又加入了几个线程来竞争对象锁,当对象锁释放的时候,这些线程之间获取到对象锁的概率是均等的,没有先来后到之理,这叫做 ——非公平锁。

    反之,如果锁竞争的线程遵守先来后到,当对象锁释放的时候,最先尝试获取对象锁失败进入等待状态的线程最先获取,那么这就叫做——公平锁。

    这些组锁概念还是很容易理解的,画个图理解一下:

    image.gif编辑

    Java 中提供的 synchronized 属于非公平锁,如果想要实现公平锁,可以在 synchronized 的基础上使用队列来记录这些线程任务,put  执行。


    1.7 synchronized 锁的属性

    synchronized 锁的特点:

    1. 既是乐观锁,也是悲观锁

    2. 既是轻量级锁,也是重量级锁

    3. 轻量级锁是基于自旋锁实现,重量级锁是基于挂起等待锁实现

    4. 不是读写锁,是互斥锁

    5. 是可重入锁

    6. 是非公平锁

    以上锁属性上文都有解析。


    到这里,Java 多线程锁策略,博主已经分享完了,希望对大家有所帮助,如有不妥之处欢迎批评指正。

    image.gif编辑

    本期收录于博主的专栏——JavaEE,适用于编程初学者,感兴趣的朋友们可以订阅,查看其它“JavaEE基础知识”。

    下期预告:CAS (compara and swap)机制

    感谢每一个观看本篇文章的朋友,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘

    相关文章
    |
    7天前
    |
    并行计算 安全 Java
    Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
    在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
    48 16
    Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
    |
    16天前
    |
    算法 安全 Java
    Java线程调度揭秘:从算法到策略,让你面试稳赢!
    在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
    55 16
    |
    2月前
    |
    Java
    【JavaEE】——多线程常用类
    Callable的call方法,FutureTask类,ReentrantLock可重入锁和对比,Semaphore信号量(PV操作)CountDownLatch锁存器,
    |
    2月前
    |
    Java 关系型数据库 MySQL
    【JavaEE“多线程进阶”】——各种“锁”大总结
    乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
    |
    2月前
    |
    Java Go 调度
    【JavaEE】——线程池大总结
    线程数量问题解决方式,代码实现线程池,ThreadPoolExecutor(核心构造方法),参数的解释(面试:拒绝策略),Executors,工厂模式,工厂类
    |
    2月前
    |
    缓存 安全 Java
    【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
    单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
    |
    2月前
    |
    Java 调度
    |
    2月前
    |
    Java 调度
    【JavaEE】——线程的安全问题和解决方式
    【JavaEE】——线程的安全问题和解决方式。为什么多线程运行会有安全问题,解决线程安全问题的思路,synchronized关键字的运用,加锁机制,“锁竞争”,几个变式
    |
    2月前
    |
    Java API 调度
    【JavaEE】——多线程(join阻塞,计算,引用,状态)
    【JavaEE】——多线程,join,sleep引起的线程阻塞,多线程提升计算效率,如何获取线程的引用和状态
    |
    2月前
    |
    Java 程序员 调度
    【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
    创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获