【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
    并发编程的艺术:Java线程与锁机制探索
    【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
    24 2
    |
    4天前
    |
    Java
    Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
    【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
    19 4
    |
    8天前
    |
    安全 Java Python
    GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
    【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
    19 6
    |
    2天前
    |
    Java
    详尽分享线程池的4种拒绝策略
    详尽分享线程池的4种拒绝策略
    |
    3天前
    |
    Java
    java线程之读写锁
    java线程之读写锁
    6 0
    |
    3天前
    |
    Java
    java线程之可重入锁
    java线程之可重入锁
    7 0
    |
    1天前
    |
    存储 测试技术
    【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
    【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
    9 0
    【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
    |
    2天前
    |
    数据采集 Java Unix
    10-多线程、多进程和线程池编程(2)
    10-多线程、多进程和线程池编程
    |
    2天前
    |
    安全 Java 调度
    10-多线程、多进程和线程池编程(1)
    10-多线程、多进程和线程池编程
    |
    7天前
    |
    存储 Linux C语言
    c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
    本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。

    热门文章

    最新文章