我们来说一说解决线程安全的方案

线程安全问题演示

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

ini 复制代码
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 是线程安全的类,使用它可以将 ++ 操作和 --- 操作,变成一个原子性操作,这样就能解决非线程安全的问题了,如下代码所示:

java 复制代码
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 层面实现的自动加锁和自动释放锁的同步锁,它的实现代码如下:

java 复制代码
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 可重入锁需要程序员自己加锁和释放锁,它的实现代码如下:

csharp 复制代码
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 线程本地变量也可以解决线程安全问题,它是给每个线程独自创建了一份属于自己的私有变量,不同的线程操作的是不同的变量,所以也不会存在非线程安全的问题,它的实现代码如下:

csharp 复制代码
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. 使用锁 synchronizedReentrantLock 加锁排队执行;
  3. 使用线程本地变量 ThreadLocal 来处理。

如果小假的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

相关推荐
镜花水月linyi21 分钟前
ConcurrentHashMap 深入解析:从0到1彻底掌握(1.3万字)
java·后端
uhakadotcom22 分钟前
Loguru 全面教程:常用 API 串联与实战指南
后端·面试·github
JuiceFS33 分钟前
JuiceFS sync 原理解析与性能优化,企业级数据同步利器
运维·后端
海边夕阳20061 小时前
主流定时任务框架对比:Spring Task/Quartz/XXL-Job怎么选?
java·后端·spring·xxl-job·定时任务·job
流水不腐5181 小时前
若依系统集成kafka
后端
allbs1 小时前
spring boot项目excel导出功能封装——3.图表导出
spring boot·后端·excel
Logan Lie2 小时前
Web服务监听地址的取舍:0.0.0.0 vs 127.0.0.1
运维·后端
程序员西西2 小时前
SpringBoot整合Apache Spark实现一个简单的数据分析功能
java·后端
shark_chili2 小时前
浅谈Java并发编程中断的哲学
后端
Billow_lamb3 小时前
Spring Boot2.x.x 全局错误处理
java·spring boot·后端