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


    线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。在 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
    从上述执行结果可以看出,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

    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

    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

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

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

    总结

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

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

    公众号:Java面试真题解析

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

  • 相关阅读:
    bat脚本%cd%和%~dp0的区别
    java测试程序运行时间
    != 的注意事项
    [转载] iptables 防火墙设置
    .NET 创建 WebService
    [转载] 学会使用Web Service上(服务器端访问)~~~
    cygwin 安装 apt-cyg
    在Element节点上进行Xpath
    Element节点输出到System.out
    [转载] 使用StAX解析xml
  • 原文地址:https://www.cnblogs.com/vipstone/p/16110663.html
Copyright © 2020-2023  润新知