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

虚幻大学 xuhss 140℃ 0评论

Python微信订餐小程序课程视频

https://edu.csdn.net/course/detail/36074

Python实战量化交易理财系统

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

  1. 使用线程安全类,比如 AtomicInteger。
  2. 加锁排队执行
    1. 使用 synchronized 加锁。
    2. 使用 ReentrantLock 加锁。
  3. 使用线程本地变量 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);
    }
}

以上程序的执行结果如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/98b89b9acac783454f55b813aa4354fc.png#clientId=u94b6bb1a-9208-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=237&id=u66380cb5&margin=[object Object]&name=image.png&originHeight=473&originWidth=2133&originalType=binary&ratio=1&rotation=0&showTitle=false&size=62571&status=done&style=none&taskId=uf63710cb-0dab-41ca-a408-967d57e04cb&title=&width=1066.5)
从上述执行结果可以看出,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());
    }
}

以上程序的执行结果如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/eac02de048d40a6bb19415d09dca665e.png#clientId=uc9b5b0b8-16f6-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=238&id=u26d96190&margin=[object Object]&name=image.png&originHeight=475&originWidth=1998&originalType=binary&ratio=1&rotation=0&showTitle=false&size=58030&status=done&style=none&taskId=ub7550b03-3b88-4f1f-8828-2eac206cc6c&title=&width=999)

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);
    }
}

以上程序的执行结果如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/878949ab633deea3e681ac6aba61a563.png#clientId=uaa1c8ab9-eb34-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=235&id=uf922960d&margin=[object Object]&name=image.png&originHeight=470&originWidth=2002&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60072&status=done&style=none&taskId=uf776a51b-e562-4a41-aea3-ac023887929&title=&width=1001)

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);
    }
}

以上程序的执行结果如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/d9d62308a188ee38080fb4920b381485.png#clientId=uaa1c8ab9-eb34-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=233&id=u59ecafbf&margin=[object Object]&name=image.png&originHeight=465&originWidth=1931&originalType=binary&ratio=1&rotation=0&showTitle=false&size=58888&status=done&style=none&taskId=u4c520a91-e5f1-4028-a55a-e2523875035&title=&width=965.5)

3.线程本地变量ThreadLocal

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

public class ThreadSafeExample {
    // 创建 ThreadLocal(设置每个线程中的初始值为 0)
    private static ThreadLocal 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);
 }
}

以上程序的执行结果如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/9fcefdc60b493a2b0ebc89c0e3a87e15.png#clientId=u1cda0e9f-319a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=239&id=u9a104fac&margin=[object Object]&name=image.png&originHeight=478&originWidth=1922&originalType=binary&ratio=1&rotation=0&showTitle=false&size=56925&status=done&style=none&taskId=u1f4e7158-55bb-4792-92da-e5e26328e6f&title=&width=961)

总结

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

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

公众号:Java面试真题解析

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

转载请注明:xuhss » 面试突击37:线程安全问题的解决方案有哪些?

喜欢 (0)

您必须 登录 才能发表评论!