写点什么

面试突击 37:线程安全问题的解决方案有哪些?

作者:王磊
  • 2022 年 4 月 07 日
  • 本文字数:3626 字

    阅读完需:约 12 分钟

面试突击37:线程安全问题的解决方案有哪些?

线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。在 Java 中,解决线程安全问题有以下 3 种手段:


  1. 使用线程安全类,比如 AtomicInteger。

  2. 加锁排队执行

  3. 使用 synchronized 加锁。

  4. 使用 ReentrantLock 加锁。

  5. 使用线程本地变量 ThreadLocal。


接下来我们逐个来看它们的实现。

线程安全问题演示

我们创建一个变量 number 等于 0,之后创建线程 1,执行 100 万次 ++ 操作,同时再创建线程 2 执行 100 万次 -- 操作,等线程 1 和线程 2 都执行完之后,打印 number 变量的值,如果打印的结果为 0,则说明是线程安全的,否则则为非线程安全的,示例代码如下:


public class ThreadSafeTest {    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException { // 线程1:执行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number++; } }); t1.start();
// 线程2:执行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number--; } }); t2.start();
// 等待线程 1 和线程 2,执行完,打印 number 最终的结果 t1.join(); t2.join(); System.out.println("number 最终结果:" + number); }}
复制代码


以上程序的执行结果如下图所示:



从上述执行结果可以看出,number 变量最终的结果并不是 0,和预期的正确结果不相符,这就是多线程中的线程安全问题。

解决线程安全问题

1.原子类 AtomicInteger

AtomicInteger 是线程安全的类,使用它可以将 ++ 操作和 -- 操作,变成一个原子性操作,这样就能解决非线程安全的问题了,如下代码所示:


import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample { // 创建 AtomicInteger private static AtomicInteger number = new AtomicInteger(0); // 循环次数 private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException { // 线程1:执行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // ++ 操作 number.incrementAndGet(); } }); t1.start();
// 线程2:执行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // -- 操作 number.decrementAndGet(); } }); t2.start();
// 等待线程 1 和线程 2,执行完,打印 number 最终的结果 t1.join(); t2.join(); System.out.println("最终结果:" + number.get()); }}
复制代码


以上程序的执行结果如下图所示:


2.加锁排队执行

Java 中有两种锁:synchronized 同步锁和 ReentrantLock 可重入锁。

2.1 同步锁 synchronized

synchronized 是 JVM 层面实现的自动加锁和自动释放锁的同步锁,它的实现代码如下:


public class SynchronizedExample {    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException { // 线程1:执行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // 加锁排队执行 synchronized (SynchronizedExample.class) { number++; } } }); t1.start();
// 线程2:执行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // 加锁排队执行 synchronized (SynchronizedExample.class) { number--; } } }); t2.start();
// 等待线程 1 和线程 2,执行完,打印 number 最终的结果 t1.join(); t2.join(); System.out.println("number 最终结果:" + number); }}
复制代码


以上程序的执行结果如下图所示:


2.2 可重入锁 ReentrantLock

ReentrantLock 可重入锁需要程序员自己加锁和释放锁,它的实现代码如下:


import java.util.concurrent.locks.ReentrantLock;
/** * 使用 ReentrantLock 解决非线程安全问题 */public class ReentrantLockExample { // 全局变量 private static int number = 0; // 循环次数(100W) private static final int COUNT = 1_000_000; // 创建 ReentrantLock private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException { // 线程1:执行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { lock.lock(); // 手动加锁 number++; // ++ 操作 lock.unlock(); // 手动释放锁 } }); t1.start();
// 线程2:执行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { lock.lock(); // 手动加锁 number--; // -- 操作 lock.unlock(); // 手动释放锁 } }); t2.start();
// 等待线程 1 和线程 2,执行完,打印 number 最终的结果 t1.join(); t2.join(); System.out.println("number 最终结果:" + number); }}
复制代码


以上程序的执行结果如下图所示:


3.线程本地变量 ThreadLocal

使用 ThreadLocal 线程本地变量也可以解决线程安全问题,它是给每个线程独自创建了一份属于自己的私有变量,不同的线程操作的是不同的变量,所以也不会存在非线程安全的问题,它的实现代码如下:


public class ThreadSafeExample {    // 创建 ThreadLocal(设置每个线程中的初始值为 0)    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException { // 线程1:执行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { try { for (int i = 0; i < COUNT; i++) { // ++ 操作 threadLocal.set(threadLocal.get() + 1); } // 将 ThreadLocal 中的值进行累加 number += threadLocal.get(); } finally { threadLocal.remove(); // 清除资源,防止内存溢出 } }); t1.start();
// 线程2:执行 100W 次 -- 操作 Thread t2 = new Thread(() -> { try { for (int i = 0; i < COUNT; i++) { // -- 操作 threadLocal.set(threadLocal.get() - 1); } // 将 ThreadLocal 中的值进行累加 number += threadLocal.get(); } finally { threadLocal.remove(); // 清除资源,防止内存溢出 } }); t2.start();
// 等待线程 1 和线程 2,执行完,打印 number 最终的结果 t1.join(); t2.join(); System.out.println("最终结果:" + number); }}
复制代码


以上程序的执行结果如下图所示:


总结

在 Java 中,解决线程安全问题的手段有 3 种:1.使用线程安全的类,如 AtomicInteger 类;2.使用锁 synchronized 或 ReentrantLock 加锁排队执行;3.使用线程本地变量 ThreadLocal 来处理。


是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java 面试真题解析

面试合集:https://gitee.com/mydb/interview

用户头像

王磊

关注

公众号:Java中文社群 2018.08.25 加入

公众号:Java中文社群

评论

发布
暂无评论
面试突击37:线程安全问题的解决方案有哪些?_Java_王磊_InfoQ写作平台