『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

简介: 『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

线程安全问题


有时候我们会发现,明明在单线程环境中正常运行的代码,在多线程环境中可能会出现意料之外的结果,其实这就是大家常说的『线程不安全』。那到底什么是线程不安全呢?往下看。


原子性

举一个银行转账的例子,比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元,两个操作都成功才意味着一次转账最终成功。

image.png

试想一下,如果这两个操作不具备原子性,从A的账户扣减了1000元之后,操作突然终止了,账户B没有增加1000元,那问题就大了。

image.png

银行转账这个例子有两个步骤,出现了意外后导致转账失败,说明没有原子性。


原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子操作:即不会被线程调度机制打断的操作,没有上下文切换。

在并发编程中很多操作都不是原子操作,出个小题目:

i = 0; // 操作1
i++;   // 操作2
i = j; // 操作3
i = i + 1; // 操作4

上面这四个操作中有哪些是原子操作,哪些不是的?不熟悉的人可能认为这些都是原子操作,其实只有操作1是原子操作。

  • 操作1:对基本数据类型变量的赋值是原子操作;
  • 操作2:包含三个操作,读取i的值,将i加1,将值赋给i;
  • 操作3:读取j的值,将j的值赋给i;
  • 操作4:包含三个操作,读取i的值,将i加1,将值赋给i;


在单线程环境下上述四个操作都不会出现问题,但是在多线程环境下,如果不通过加锁操作,往往可能得到意料之外的值。


在Java语言中通过可以使用synchronize或者lock来保证原子性。


可见性

talk is cheap,先show一段代码:

/**
* Author: leixiaoshuai
*/
class Test {
  int i = 50;
  int j = 0;
  public void update() {
    // 线程1执行
    i = 100;
  }
  public int get() {
    // 线程2执行
    j = i;
    return j;
  }
}

线程1执行update方法将 i 赋值为100,一般情况下线程1会在自己的工作内存中完成赋值操作,却没有及时将新值刷新到主内存中。


这个时候线程2执行get方法,首先会从主内存中读取i的值,然后加载到自己的工作内存中,这个时候读取到i的值是50,再将50赋值给j,最后返回j的值就是50了。原本期望返回100,结果返回50,这就是可见性问题,线程1对变量i进行了修改,线程2没有立即看到i的新值。


可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

image.png

如上图每个线程都有属于自己的工作内存,工作内存和主内存间需要通过store和load等进行交互。


为了解决多线程可见性问题,Java语言提供了volatile这个关键字。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通共享变量不能保证可见性,因为变量被修改后什么时候刷回到主存是不确定的,另外一个线程读的可能就是旧值。


当然Java的锁机制如synchronize和lock也是可以保证可见性的,加锁可以保证在同一时刻只有一个线程在执行同步代码块,释放锁之前会将变量刷回至主存,这样也就保证了可见性。


关于线程不安全的表现还有『有序性』,这个问题会在后面的文章中深入讲解。


活跃性问题


上面讲到为了解决可见性问题,我们可以采取加锁方式解决,但是如果加锁使用不当也容易引入其他问题,比如『死锁』。


在说『死锁』前我们先引入另外一个概念:活跃性问题。


活跃性是指某件正确的事情最终会发生,当某个操作无法继续下去的时候,就会发生活跃性问题。


概念是不是有点拗口,如果看不懂也没关系,你可以记住活跃性问题一般有这样几类:死锁,活锁,饥饿问题。


(1)死锁

死锁是指多个线程因为环形的等待锁的关系而永远的阻塞下去。一图胜千语,不多解释。

image.png

(2)活锁

死锁是两个线程都在等待对方释放锁导致阻塞。而活锁的意思是线程没有阻塞,还活着呢。


当多个线程都在运行并且修改各自的状态,而其他线程彼此依赖这个状态,导致任何一个线程都无法继续执行,只能重复着自身的动作和修改自身的状态,这种场景就是发生了活锁。


![](/Users/ray/Library/Application Support/typora-user-images/image-20210408232019843.png)


如果大家还有疑惑,那我再举一个生活中的例子,大家平时在走路的时候,迎面走来一个人,两个人互相让路,但是又同时走到了一个方向,如果一直这样重复着避让,这俩人就是发生了活锁,学到了吧,嘿嘿。


(3)饥饿

如果一个线程无其他异常却迟迟不能继续运行,那基本是处于饥饿状态了。

常见有几种场景:

  • 高优先级的线程一直在运行消耗CPU,所有的低优先级线程一直处于等待;
  • 一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;


有一个非常经典的饥饿问题就是哲学家用餐问题,如下图所示,有五个哲学家在用餐,每个人必须要同时拿两把叉子才可以开始就餐,如果哲学家1和哲学家3同时开始就餐,那哲学家2、4、5就得饿肚子等待了。

image.png


性能问题


前面讲到了线程安全和死锁、活锁这些问题会影响多线程执行过程,如果这些都没有发生,多线程并发一定比单线程串行执行快吗,答案是不一定,因为多线程有创建线程和线程上下文切换的开销。


创建线程是直接向系统申请资源的,对操作系统来说创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。


线程创建完之后,还会遇到线程上下文切换。

image.png

CPU是很宝贵的资源速度也非常快,为了保证雨露均沾,通常为给不同的线程分配时间片,当CPU从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个开关被称为『上下文切换』。


一般减少上下文切换的方法有:无锁并发编程、CAS 算法、使用协程等。


有态度的总结


多线程用好了可以让程序的效率成倍提升,用不好可能比单线程还要慢。


用一张图总结一下上面讲的:

image.png

文章讲了多线程并发会遇到的问题,你可能也发现了,文章中并没有给出具体的解决方案,因为这些问题在Java语言设计过程中大神都已经为你考虑过了。


Java并发编程学起来有一定难度,但这也是从初级程序员迈向中高级程序员的必经道路,接下来的文章会带领大家逐个击破!


相关文章
|
1天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
6 1
|
3天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
3天前
|
SQL 安全 Java
Java安全编程:防范网络攻击与漏洞
【4月更文挑战第15天】本文强调了Java安全编程的重要性,包括提高系统安全性、降低维护成本和提升用户体验。针对网络攻击和漏洞,提出了防范措施:使用PreparedStatement防SQL注入,过滤和转义用户输入抵御XSS攻击,添加令牌对抗CSRF,限制文件上传类型和大小以防止恶意文件,避免原生序列化并确保数据完整性。及时更新和修复漏洞是关键。程序员应遵循安全编程规范,保障系统安全。
|
4天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
5天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
12 4
|
6天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
14 1
探秘jstack:解决Java应用线程问题的利器
|
6天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
14 0
|
10天前
|
Java
Java 并发编程:深入理解线程池
【4月更文挑战第8天】本文将深入探讨 Java 中的线程池技术,包括其工作原理、优势以及如何使用。线程池是 Java 并发编程的重要工具,它可以有效地管理和控制线程的执行,提高系统性能。通过本文的学习,读者将对线程池有更深入的理解,并能在实际开发中灵活运用。
|
1月前
|
存储 Java 程序员
Java并发编程:深入理解线程池
【2月更文挑战第14天】 在现代软件开发中,高效地处理并发任务已成为提升性能和响应速度的关键。Java作为广泛使用的编程语言,其内置的并发工具特别是线程池机制,为开发者提供了强大的支持。本文将深入探讨Java线程池的核心概念、工作机制以及如何合理配置线程池以适应不同的应用场景。我们将通过理论解析与实践案例相结合的方式,使读者不仅理解线程池的工作原理,还能掌握其在复杂系统中的高效应用。
22 0
|
29天前
|
监控 Java
Java并发编程中的线程池优化技巧
在Java并发编程中,线程池扮演着至关重要的角色。本文将深入探讨如何优化Java线程池,从线程池的创建与配置、任务队列的选择、拒绝策略的制定、线程池状态的监控等多个方面进行详细阐述。通过本文的阅读,您将了解到如何合理地利用线程池,提高系统的并发性能,从而更好地应对各种并发场景。