Python微信订餐小程序课程视频
https://edu.csdn.net/course/detail/36074
Python实战量化交易理财系统
https://edu.csdn.net/course/detail/35475
线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。在 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);
}
}
以上程序的执行结果如下图所示:
![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面试真题解析
转载请注明:xuhss » 面试突击37:线程安全问题的解决方案有哪些?