【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)机制

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

    相关文章
    |
    13天前
    |
    Java
    线程池内部机制:线程的保活与回收策略
    【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
    40 2
    |
    26天前
    |
    调度 Android开发 开发者
    构建高效Android应用:探究Kotlin多线程优化策略
    【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
    41 4
    |
    28天前
    |
    Java 应用服务中间件 测试技术
    Java21虚拟线程:我的锁去哪儿了?
    【10月更文挑战第8天】
    31 0
    |
    1月前
    |
    安全 调度 数据安全/隐私保护
    iOS线程锁
    iOS线程锁
    25 0
    |
    1月前
    |
    Java API
    【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
    【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
    28 0
    |
    1月前
    |
    安全 Java 程序员
    【多线程-从零开始-肆】线程安全、加锁和死锁
    【多线程-从零开始-肆】线程安全、加锁和死锁
    42 0
    |
    1月前
    |
    存储 消息中间件 资源调度
    C++ 多线程之初识多线程
    这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
    39 1
    C++ 多线程之初识多线程
    |
    17天前
    |
    Java 开发者
    在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
    【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
    13 3
    |
    17天前
    |
    Java 开发者
    在Java多线程编程中,选择合适的线程创建方法至关重要
    【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
    13 2
    |
    17天前
    |
    Java
    Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
    【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
    27 2