Hi,大家好,我是小米,一个喜欢分享技术的程序员大哥哥!最近有个学弟来找我,说在面试某大厂Java岗位时被问了个问题:“并发编程三要素是什么?在Java中如何保证多线程运行的安全?”学弟被问懵了,不知道怎么答。没关系!今天咱们就来聊聊这个经典问题!
并发编程三要素是什么?
在多线程编程中,最重要的三大核心要素是 原子性、可见性 和 有序性。这三个概念是并发编程的基础,掌握它们可以帮助我们更好地理解线程安全问题。
1. 原子性(Atomicity)
原子性指的是一个操作要么完全执行,要么完全不执行,不会出现中间状态。
比如,银行转账时,假设 A 给 B 转100块,分两步:
- 从 A 的账户扣 100 块;
- 往 B 的账户加 100 块。
如果这两个操作中间被打断,导致只执行了第一个操作,钱就“丢”了。所以,我们需要保证整个转账操作的原子性。
在 Java 中,synchronized 和 Lock 是保证原子性的重要工具。
2. 可见性(Visibility)
可见性指的是当一个线程修改了共享变量的值,其他线程能够及时知道这个修改。
Java 中每个线程都有自己的工作内存,当线程 A 修改了一个变量,线程 B 并不知道,因为 B 可能还在用它的旧值。这种情况下就会发生数据不一致的问题。
为了解决可见性问题,Java 提供了 volatile 关键字,以及 synchronized 和 Lock 机制。
3. 有序性(Orderliness)
有序性指的是程序的执行顺序按照代码的书写顺序来运行。
不过,在并发编程中,编译器和处理器可能会为了优化性能而重排序,导致程序执行的顺序与代码顺序不同。
Java 提供了 volatile、synchronized 和 内存屏障 来避免重排序问题。
在 Java 中如何保证多线程运行安全?
多线程的安全问题可以概括为一句话:保证并发访问的共享资源在任何时候都是一致的。接下来,我们看看几种保证线程安全的方式。
1. 使用 synchronized
synchronized 是 Java 提供的最基础的锁机制,用于保证同一时刻只有一个线程可以访问某段代码。比如:
上面的 withdraw 和 getBalance 方法都被 synchronized 修饰,保证了对 balance 的访问是线程安全的。
2. 使用 Lock
Java 的 java.util.concurrent.locks 包提供了更灵活的锁机制,比如 ReentrantLock。它和 synchronized 的区别在于:
- 可以尝试加锁(tryLock);
- 可以定时加锁;
- 支持中断。
示例代码:
3. 使用 volatile
当我们只需要保证可见性而不是原子性时,可以使用 volatile。例如:
volatile 确保 running 的修改对其他线程是可见的。
4. 使用 原子类
Java 提供了一系列原子类(AtomicInteger, AtomicLong, 等),用于保证操作的原子性。例如:
这里的 incrementAndGet 方法是线程安全的。
5. 使用线程安全的集合
在并发环境下操作集合时,可以使用 java.util.concurrent 包下的线程安全集合类,比如:
- ConcurrentHashMap
- CopyOnWriteArrayList
- BlockingQueue
示例代码:
这些集合类在内部已经实现了线程安全机制,避免了手动加锁的麻烦。
总结
并发编程三要素:
- 原子性 - 保证操作不可分割。
- 可见性 - 保证线程间的修改是可见的。
- 有序性 - 保证代码按预期顺序执行。
多线程安全的解决方案:
- 使用 synchronized 或 Lock 来控制线程访问;
- 使用 volatile 解决可见性问题;
- 使用原子类处理简单的原子性操作;
- 使用线程安全的集合类简化开发。
END
希望通过这篇文章,大家能更好地理解并发编程三要素以及如何用 Java 编写线程安全的程序。如果觉得有收获,不要忘了点个赞或者转发给身边的朋友哦!有任何问题,也欢迎在评论区留言,我们一起探讨~
我是小米,咱们下次见啦!
我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!