首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 01 | 可见性、原子性和有序性问题:并发编程Bug的源头 在深入探讨Java并发编程的广阔领域时,我们不得不首先面对其根基上的三大挑战:可见性(Visibility)、原子性(Atomicity)和有序性(Ordering)。这三大问题不仅是并发编程中错误和bug的主要来源,也是理解高级并发工具和模式的基础。本章节将逐一剖析这些问题,帮助读者构建坚实的并发编程理论基础。 #### 一、可见性问题:数据共享的迷雾 在并发编程中,多个线程可能同时访问和操作共享数据。然而,由于Java内存模型(Java Memory Model, JMM)的设计,这些线程看到的内存视图可能是不一致的,即所谓的“可见性问题”。简单来说,一个线程对共享变量的修改,对于其他线程而言可能并不是立即可见的。 ##### 1.1 JMM基础 Java内存模型定义了线程和主内存之间的抽象关系,以及线程之间共享变量的存储和交互方式。每个线程都有自己的工作内存(Work Memory),用于存储该线程操作共享变量的本地副本。当线程需要读取或写入共享变量时,它首先会在自己的工作内存中操作这些变量的副本,然后再将更改同步回主内存(或从其他线程的工作内存中读取更改)。这种机制虽然提高了效率,但也带来了可见性问题。 ##### 1.2 可见性问题的表现 - **脏读**:一个线程读取了另一个线程未提交(即未同步回主内存)的数据。 - **丢失更新**:两个线程同时读取了某个共享变量的值,然后各自基于该值进行了修改,但只有一个修改结果被同步回主内存,导致另一个修改丢失。 ##### 1.3 解决方案 - **volatile关键字**:标记为volatile的变量,其每次读写操作都直接作用于主内存,从而确保所有线程都能看到最新的值。但需注意,volatile不能解决复合操作的原子性问题。 - **锁(Locks)**:通过加锁机制,确保同一时刻只有一个线程能访问共享变量,从而避免了脏读和丢失更新等问题。 - **原子变量(Atomic Variables)**:Java并发包(`java.util.concurrent.atomic`)提供了一系列原子变量类,这些类利用底层的CAS(Compare-And-Swap)操作,实现了对单个变量操作的原子性,同时保证了操作的可见性。 #### 二、原子性问题:操作不可分割的保障 原子性指的是一个操作或多个操作要么全部执行,要么完全不执行,中间状态对外部不可见。在并发环境下,原子性问题尤为突出,因为即使是简单的操作(如自增),在多线程环境中也可能因为指令重排等原因而变得非原子。 ##### 2.1 原子性问题的示例 考虑一个简单的自增操作`count++`,在Java中,这实际上是一个复合操作,包括读取`count`的值、加1、再写回`count`。在没有同步措施的情况下,如果有两个线程同时执行这个操作,它们可能会读取到相同的初始值,然后各自加1后再写回,最终导致`count`只增加了1而不是预期的2。 ##### 2.2 解决方案 - **同步代码块(Synchronized Blocks)**:使用`synchronized`关键字可以确保一个代码块在同一时刻只能被一个线程执行,从而保证了该代码块内所有操作的原子性。 - **原子类**:Java并发包中的原子类提供了非阻塞的线程安全操作,如`AtomicInteger`、`AtomicLong`等,它们通过底层的CAS操作实现了对单个变量的原子操作。 - **锁(Locks)**:除了`synchronized`关键字外,Java还提供了显式锁(如`ReentrantLock`),它们提供了比`synchronized`更灵活的锁定机制,同样能够保证操作的原子性。 #### 三、有序性问题:指令执行的混乱 有序性问题指的是程序中的操作执行顺序可能与编写时的顺序不一致。这主要是由于现代处理器为了提高性能而采用的指令重排(Instruction Reordering)技术。在单线程环境下,这种优化通常是无害的,但在多线程环境下,它可能导致数据竞争和不可预测的行为。 ##### 3.1 有序性问题的根源 - **指令重排**:编译器和处理器为了提高效率,可能会对程序中的指令进行重新排序,只要这种重排不违反单线程语义。 - **内存访问的乱序**:由于缓存和主内存之间的同步延迟,线程对共享变量的读写操作可能会以非预期的顺序发生。 ##### 3.2 解决方案 - **volatile关键字**:如前所述,`volatile`关键字不仅保证了变量的可见性,还禁止了指令重排在volatile变量访问上的应用,从而确保了有序性。 - **锁(Locks)**:通过加锁,可以确保在同一时刻只有一个线程能执行特定代码段,从而间接地保证了该代码段内指令的有序执行。 - **Happens-Before规则**:Java内存模型定义了一系列“Happens-Before”规则,这些规则规定了哪些操作必须在哪些操作之前完成,从而保证了多线程程序中的有序性。了解和利用这些规则,对于编写高效的并发程序至关重要。 #### 结语 可见性、原子性和有序性问题是并发编程中不可回避的挑战。理解这些问题及其背后的原理,掌握相应的解决方案,是编写健壮、高效并发程序的关键。通过合理使用`volatile`、锁、原子变量等同步机制,我们可以有效地避免并发编程中的常见bug,提高程序的可靠性和性能。同时,深入理解Java内存模型及其“Happens-Before”规则,也将有助于我们编写出更加优雅、易于维护的并发代码。在后续的章节中,我们将进一步探讨Java并发编程中的高级话题,如线程池、并发集合、锁的高级特性等,帮助读者构建全面的并发编程知识体系。
下一篇:
02 | Java内存模型:看Java如何解决可见性和有序性问题
该分类下的相关小册推荐:
Java语言基础14-枚举和注解
SpringBoot零基础到实战
经典设计模式Java版
Java语言基础5-面向对象初级
SpringBoot合辑-高级篇
Java必知必会-Maven初级
Mybatis合辑4-Mybatis缓存机制
Java语言基础7-Java中的异常
Java高并发秒杀入门与实战
Java语言基础16-JDK8 新特性
Java语言基础15-单元测试和日志技术
Java语言基础8-Java多线程