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

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

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

  • 使用线程安全类,比如 AtomicInteger。
  • 加锁排队执行

使用 synchronized 加锁。

使用 ReentrantLock 加锁。

  • 使用线程本地变量 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.原子类AtomicIntege

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面试真题解析
相关推荐

2022-04-06 07:50:28

线程安全代码

2023-02-28 07:40:09

编译器Java线程安全

2019-04-02 08:20:37

2023-03-24 15:06:03

2019-06-14 05:00:05

2009-11-12 15:05:13

USB移动安全解决方案

2009-07-13 10:36:18

2009-10-28 11:27:49

linux服务器安全

2022-03-23 08:51:21

线程池Java面试题

2020-04-10 08:34:58

网络安全邮件安全网络钓鱼

2022-04-18 07:36:37

TimeUnit线程休眠

2022-06-06 07:35:26

MySQLInnoDBMyISAM

2022-05-11 07:41:55

死锁运算线程

2022-06-01 12:00:54

HTTP状态码服务端

2022-01-24 07:01:20

安全多线程版本

2023-12-17 14:19:57

2009-07-15 17:09:32

Swing线程

2022-02-08 07:02:32

进程线程操作系统

2020-03-11 09:57:10

数据安全网络安全网络攻击

2021-05-17 07:51:44

SimpleDateF线程安全
点赞
收藏

51CTO技术栈公众号