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

  1. 使用线程安全类,比如 AtomicInteger
  2. 加锁排队执行
    1. 使用 synchronized 加锁。
    2. 使用 ReentrantLock 加锁。
  3. 使用线程本地变量 ThreadLocal

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

线程安全问题演示

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

  1. public class ThreadSafeTest {
  2. // 全局变量
  3. private static int number = 0;
  4. // 循环次数(100W)
  5. private static final int COUNT = 1_000_000;
  6. public static void main(String[] args) throws InterruptedException {
  7. // 线程1:执行 100W 次 ++ 操作
  8. Thread t1 = new Thread(() -> {
  9. for (int i = 0; i < COUNT; i++) {
  10. number++;
  11. }
  12. });
  13. t1.start();
  14. // 线程2:执行 100W 次 -- 操作
  15. Thread t2 = new Thread(() -> {
  16. for (int i = 0; i < COUNT; i++) {
  17. number--;
  18. }
  19. });
  20. t2.start();
  21. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  22. t1.join();
  23. t2.join();
  24. System.out.println("number 最终结果:" + number);
  25. }
  26. }

以上程序的执行结果如下图所示:线程安全问题的解决方案 - 图1从上述执行结果可以看出,number 变量最终的结果并不是 0,和预期的正确结果不相符,这就是多线程中的线程安全问题。

解决线程安全问题

1、原子类AtomicInteger

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

  1. import java.util.concurrent.atomic.AtomicInteger;
  2. public class AtomicIntegerExample {
  3. // 创建 AtomicInteger
  4. private static AtomicInteger number = new AtomicInteger(0);
  5. // 循环次数
  6. private static final int COUNT = 1_000_000;
  7. public static void main(String[] args) throws InterruptedException {
  8. // 线程1:执行 100W 次 ++ 操作
  9. Thread t1 = new Thread(() -> {
  10. for (int i = 0; i < COUNT; i++) {
  11. // ++ 操作
  12. number.incrementAndGet();
  13. }
  14. });
  15. t1.start();
  16. // 线程2:执行 100W 次 -- 操作
  17. Thread t2 = new Thread(() -> {
  18. for (int i = 0; i < COUNT; i++) {
  19. // -- 操作
  20. number.decrementAndGet();
  21. }
  22. });
  23. t2.start();
  24. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  25. t1.join();
  26. t2.join();
  27. System.out.println("最终结果:" + number.get());
  28. }
  29. }

以上程序的执行结果如下图所示:线程安全问题的解决方案 - 图2

2、加锁排队执行

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

2.1 同步锁synchronized

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

  1. public class SynchronizedExample {
  2. // 全局变量
  3. private static int number = 0;
  4. // 循环次数(100W)
  5. private static final int COUNT = 1_000_000;
  6. public static void main(String[] args) throws InterruptedException {
  7. // 线程1:执行 100W 次 ++ 操作
  8. Thread t1 = new Thread(() -> {
  9. for (int i = 0; i < COUNT; i++) {
  10. // 加锁排队执行
  11. synchronized (SynchronizedExample.class) {
  12. number++;
  13. }
  14. }
  15. });
  16. t1.start();
  17. // 线程2:执行 100W 次 -- 操作
  18. Thread t2 = new Thread(() -> {
  19. for (int i = 0; i < COUNT; i++) {
  20. // 加锁排队执行
  21. synchronized (SynchronizedExample.class) {
  22. number--;
  23. }
  24. }
  25. });
  26. t2.start();
  27. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  28. t1.join();
  29. t2.join();
  30. System.out.println("number 最终结果:" + number);
  31. }
  32. }

以上程序的执行结果如下图所示:线程安全问题的解决方案 - 图3

2.2 可重入锁ReentrantLock

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

  1. import java.util.concurrent.locks.ReentrantLock;
  2. /**
  3. * 使用 ReentrantLock 解决非线程安全问题
  4. */
  5. public class ReentrantLockExample {
  6. // 全局变量
  7. private static int number = 0;
  8. // 循环次数(100W)
  9. private static final int COUNT = 1_000_000;
  10. // 创建 ReentrantLock
  11. private static ReentrantLock lock = new ReentrantLock();
  12. public static void main(String[] args) throws InterruptedException {
  13. // 线程1:执行 100W 次 ++ 操作
  14. Thread t1 = new Thread(() -> {
  15. for (int i = 0; i < COUNT; i++) {
  16. lock.lock(); // 手动加锁
  17. number++; // ++ 操作
  18. lock.unlock(); // 手动释放锁
  19. }
  20. });
  21. t1.start();
  22. // 线程2:执行 100W 次 -- 操作
  23. Thread t2 = new Thread(() -> {
  24. for (int i = 0; i < COUNT; i++) {
  25. lock.lock(); // 手动加锁
  26. number--; // -- 操作
  27. lock.unlock(); // 手动释放锁
  28. }
  29. });
  30. t2.start();
  31. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  32. t1.join();
  33. t2.join();
  34. System.out.println("number 最终结果:" + number);
  35. }
  36. }

以上程序的执行结果如下图所示:线程安全问题的解决方案 - 图4

3、线程本地变量ThreadLocal

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

  1. public class ThreadSafeExample {
  2. // 创建 ThreadLocal(设置每个线程中的初始值为 0)
  3. private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
  4. // 全局变量
  5. private static int number = 0;
  6. // 循环次数(100W)
  7. private static final int COUNT = 1_000_000;
  8. public static void main(String[] args) throws InterruptedException {
  9. // 线程1:执行 100W 次 ++ 操作
  10. Thread t1 = new Thread(() -> {
  11. try {
  12. for (int i = 0; i < COUNT; i++) {
  13. // ++ 操作
  14. threadLocal.set(threadLocal.get() + 1);
  15. }
  16. // 将 ThreadLocal 中的值进行累加
  17. number += threadLocal.get();
  18. } finally {
  19. threadLocal.remove(); // 清除资源,防止内存溢出
  20. }
  21. });
  22. t1.start();
  23. // 线程2:执行 100W 次 -- 操作
  24. Thread t2 = new Thread(() -> {
  25. try {
  26. for (int i = 0; i < COUNT; i++) {
  27. // -- 操作
  28. threadLocal.set(threadLocal.get() - 1);
  29. }
  30. // 将 ThreadLocal 中的值进行累加
  31. number += threadLocal.get();
  32. } finally {
  33. threadLocal.remove(); // 清除资源,防止内存溢出
  34. }
  35. });
  36. t2.start();
  37. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  38. t1.join();
  39. t2.join();
  40. System.out.println("最终结果:" + number);
  41. }
  42. }

以上程序的执行结果如下图所示:线程安全问题的解决方案 - 图5

总结

在 Java 中,解决线程安全问题的手段有 3 种:

  1. 使用线程安全的类,如 AtomicInteger 类;
  2. 使用锁 synchronizedReentrantLock 加锁排队执行;
  3. 使用线程本地变量 ThreadLocal 来处理。