Java多线程【锁优化与死锁】

简介: Java多线程【锁优化与死锁】

🍒一.synchronized的优化


我们延续上一篇博客进行锁优化的讲解


3a57d8d847a64c24af03effb1cf22b0b.png

🍎1.1基本特点


结合上一篇博客的锁策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):

我们根据上述锁策略可知synchronized锁基本特点

1.既是一个乐观锁也是悲观锁(根据锁竞争的的程度,自适应)

2.不是读写锁,只是一个互斥锁

3.既是一个轻量级锁,也是一个重量级锁(根据锁竞争的的程度,自适应)

4.轻量级锁部分由自旋锁来实现的,重量级锁部分是由挂起等待锁来实现的

5.非公平锁

6.可重入锁


🍎1.2加锁工作过程


JVM 将 synchronized 锁分为 无锁→偏向锁轻量级锁→重量级锁状态,会根据情况进行依次升级


一.偏向锁:


第一个尝试加锁的线程, 优先进入偏向锁状态.

偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.


举个栗子理解偏向锁:

假设男主是一个锁, 女主是一个线程. 如果只有这一个线程来使用这个锁, 那么男主女主即使不领证结婚(避免了高成本操作), 也可以一直幸福的生活下去.但是女配出现了, 也尝试竞争男主, 此时不管领证结婚这个操作成本多高, 女主也势必要把这个动作完成了, 让女配死心.


二.轻量级锁:

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁),此处的轻量级锁就是通过 CAS 来实现.

●通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)

●如果更新成功, 则认为加锁成功

●如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源,因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.也就是所谓的 “自适应”


三.重量级锁:

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁此处的重量级锁就是指用到内核提供的 mutex .

执行加锁操作, 先进入内核态.

●在内核态判定当前锁是否已经被占用

●如果该锁没有占用, 则加锁成功, 并切换回用户态.

●如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.

●经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.


🍎1.3优化操作


一.锁消除


编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.

什么是 “锁消除”

有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销.


StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");


二.锁粗化,锁细化


一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
一段逻辑中如果未出现多次加锁解锁, 编译器 + JVM 会自动进行锁的细化.

锁的粒度: 粗和细


实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.

举个栗子理解锁粗化

滑稽老哥当了领导, 给下属交代工作任务:

方式一:

打电话, 交代任务1, 挂电话.

打电话, 交代任务2, 挂电话.

打电话, 交代任务3, 挂电话.

方式二:

打电话, 交代任务1, 任务2, 任务3, 挂电话.

显然, 方式二是更高效的方案.


🍒二.死锁


🍎2.1什么是死锁


死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止


🍎2.2 一对一死锁


一个线程与一把锁,这个线程多次使用了者一把锁进行加锁,当想把第二把锁解开就需要把第一把锁解开,可是第一把锁要解开就需要第二把锁解开才能解开第一把锁…

这个就会联想到之前的不可重入锁


🍎2.3 二对二死锁


两个线程与两把锁

举个栗子理解:

滑稽老哥和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋.

滑稽老哥抄起了酱油瓶, 女神抄起了醋瓶.

滑稽: 你先把醋瓶给我, 我用完了就把酱油瓶给你.

女神: 你先把酱油瓶给我, 我用完了就把醋瓶给你.

如果这俩人彼此之间互不相让, 就构成了死锁.

酱油和醋相当于是两把锁, 这两个人就是两个线程


🍎2.4 N对M死锁


假设我们有一桌等待吃饭的5个贪吃鬼,但是只有5个筷子



9ee0de716441428985815992baa36796.png


这时,我们让他们每个人拿到身边一个筷子,我们发现他们虽然都有一个筷子,但是都需要等待别人同时有两个筷子用完后,才能得到另一个筷子变成一双筷子才能吃饭,这时就是死锁了



891cfc402c0c430e9a9248e9159b5148.png



这时我们可以让5个贪吃鬼分别拿离自己最小序号的筷子就可以解决死锁了

506291e420bc4682ab5f5ef8e8b49efd.png



🍎2.5死锁的条件


死锁产生的四个必要条件:

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

●不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放

●请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有

●循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路


当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破坏的就是 “循环等待”.

相关文章
|
7天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
27 9
|
10天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
7天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
8天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
6月前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
58 0
|
6月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
3月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
64 1
|
4月前
|
安全 Java 开发者
Java并发编程中的线程安全问题及解决方案探讨
在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
93 0
|
5月前
|
安全 Java 开发者
Java并发编程中的线程安全策略
在现代软件开发中,Java语言的并发编程特性使得多线程应用成为可能。然而,随着线程数量的增加,如何确保数据的一致性和系统的稳定性成为开发者面临的挑战。本文将探讨Java并发编程中实现线程安全的几种策略,包括同步机制、volatile关键字的使用、以及java.util.concurrent包提供的工具类,旨在为Java开发者提供一系列实用的方法来应对并发问题。
45 0
|
6月前
|
安全 Java 容器
Java一分钟之-并发编程:线程安全的集合类
【5月更文挑战第19天】Java提供线程安全集合类以解决并发环境中的数据一致性问题。例如,Vector是线程安全但效率低;可以使用Collections.synchronizedXxx将ArrayList或HashMap同步;ConcurrentHashMap是高效线程安全的映射;CopyOnWriteArrayList和CopyOnWriteArraySet适合读多写少场景;LinkedBlockingQueue是生产者-消费者模型中的线程安全队列。注意,过度同步可能影响性能,应尽量减少共享状态并利用并发工具类。
64 2