ToB企服应用市场:ToB评测及商务社交产业平台
标题:
【JavaEE】线程安全性题目,线程不安满是怎么产生的,该如何应对
[打印本页]
作者:
宁睿
时间:
2024-11-21 20:55
标题:
【JavaEE】线程安全性题目,线程不安满是怎么产生的,该如何应对
产生线程不安全的原因
在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因:
共享可变状态
:当多个线程对共享的可变数据进行读写时,如果没有适当的同步机制,可能导致数据的不一致性。比方,两个线程同时修改一个共享变量,最终的结果可能取决于线程的执行次序。
缺乏同步
:在没有利用synchronized关键字或其他同步机制(如Lock)进行保护的情况下,多个线程可以同时进入临界区,从而导致线程安全题目。
指令重排序
:为了进步执行效率,Java假造机和处理器可能会对指令进行重排序,这种行为在多线程环境中可能导致不可预期的结果,尤其是在多个线程依赖某些变量的状态时。
原子性题目
:某些操作在Java中并不是原子的,比方对对象属性的读-改-写操作。在多线程环境下,这类操作必须通过同步处理以确保原子性。
死锁
:只管死锁本身不直接导致线程不安全,但在复杂的同步情况下,死锁可能导致某些线程无法继续执行,从而影响整体程序的精确性与稳定性。
不可见性
:当一个线程对共享变量的修改在其他线程中不可见时,可能导致一些线程读取到逾期的值。这通常可以通过利用volatile关键字来解决。
产生线程不安全的案例以及应对方法
共享可变状态案例
我们将创建一个简单的银行账户类,多个线程并发访问该账户进行存款和取款操作。假设我们有两个线程同时对账户进行操作,可能会出现余额盘算错误的情况。
class BankAccount {
private int balance = 100; // 初始余额为100
public void deposit(int amount) {
balance += amount; // 存款
}
public void withdraw(int amount) {
balance -= amount; // 取款
}
public int getBalance() {
return balance; // 返回当前余额
}
}
public class UnsafeBank {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 创建两个线程同时操作
Thread t1 = new Thread(() -> {
account.withdraw(50);
System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());
});
Thread t2 = new Thread(() -> {
account.deposit(30);
System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());
});
t1.start();
t2.start();
}
}
复制代码
运行情况:
我们盼望的运行结果是:取款50,余额50、存款30,余额80。但是上述结果并不是我们想要的
分析
在上述代码中,两个线程同时对balance变量进行操作,可能导致不一致的余额输出。比方,假设Thread 1先读取了余额为100,然后进行了取款操作,但在它更新余额之前,Thread 2可能已经读取了余额并进行了存款操作。最终的结果可能不符合预期。
解决方法
为相识决这个线程不安全的题目,我们可以利用synchronized关键字来确保对共享资源的访问是线程安全的。我们可以对deposit和withdraw方法加锁,使得同一时间只有一个线程能够执行此中一个方法。
以下是修改后的代码:
class BankAccount {
private int balance = 100; // 初始余额为100
// 存款操作
public synchronized void deposit(int amount) {
balance += amount; // 存款
}
// 取款操作
public synchronized void withdraw(int amount) {
balance -= amount; // 取款
}
// 返回当前余额
public int getBalance() {
return balance; // 返回当前余额
}
}
public class SafeBank {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount();
// 创建两个线程同时操作
Thread t1 = new Thread(() -> {
account.withdraw(50);
System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());
});
Thread t2 = new Thread(() -> {
account.deposit(30);
System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());
});
t1.start();
t2.start();
// 等待两个线程结束
t1.join();
t2.join();
// 输出最终余额
System.out.println("Final balance: " + account.getBalance());
}
}
复制代码
结果
在修改后的代码中,由于对deposit和withdraw方法加了synchronized修饰,确保任何时刻只有一个线程可以执行这两个方法,从而避免了由于竞争条件导致的不一致性。最终输出的余额将与预期结果相一致。
指令重排序案例
指令重排序是指在编译、优化或CPU执行过程中,代码的执行次序被改变。
count++操作并不是一个原子操作,它是由三个步骤构成的:
读取当前的值。
对值加1。
将新值写回。
在多线程环境中,多个线程可能会同时对同一变量进行count++操作,导致结果不精确。这种情况下,指令重排序可能导致某些操作无法达到预期结果。
以下是一个示例代码,演示了这个题目:
class Counter {
private int count = 0;
public void increment() {
count++; // 不安全的操作
}
public int getCount() {
return count;
}
}
public class CountExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment(); // 增加计数
}
});
}
// 启动所有线程
for (Thread thread : threads) {
thread.start();
}
// 等待所有线程结束
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数
System.out.println("Final count: " + counter.getCount());
}
}
复制代码
运行结果:
我们的预期结果是:10000
分析
在上述代码中,我们创建了10个线程,每个线程执行1000次increment()方法,从而盼望最终的计数是10000。然而,由于count++操作的非原子性,在多个线程并发执行时,可能会导致某些增量操作丢失,最闭幕果可能小于10000。
解决方法
为相识决这个题目,可以利用以下几种方法:
利用synchronized关键字
:将increment方法同步,以确保同一时刻只有一个线程能执行该操作。
**利用AtomicInteger**
:Java提供了原子类AtomicInteger,能够保证对整数操作的原子性。
我们将采用第二种方法,即利用AtomicInteger来解决这个题目。
以下是修改后的代码:
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0); // 使用AtomicInteger
public void increment() {
count.incrementAndGet(); // 原子性增加
}
public int getCount() {
return count.get(); // 获取当前值
}
}
public class SafeCountExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment(); // 增加计数
}
});
}
// 启动所有线程
for (Thread thread : threads) {
thread.start();
}
// 等待所有线程结束
for (Thread thread : threads) {
thread.join();
}
// 输出最终计数
System.out.println("Final count: " + counter.getCount());
}
}
复制代码
不可见性案例
我们利用了两个线程t1和t2。线程t1负责不停地查抄一个共享变量fag,而线程t2则在休眠1秒后将fag设为1。
public class Main {
public static int fag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (fag == 0) {
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
fag = 1;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程结束");
}
}
复制代码
分析
在Java中,fag是一个共享的静态变量,初始值为0。线程t1在一个循环中不断查抄fag的值,而线程t2在休眠1秒后将fag更新为1。根据Java内存模子的规定,线程可以在运行过程中缓存某些变量,以进步性能。这意味着,线程t1可能在本身的工作内存中读取到fag的值,而且不会每次都去主内存中查抄当其值变化时。
因此,虽然t2可能已经将fag设置为1,但如果t1线程没有看到这个变化,它仍然可能会在其循环中继续查看到fag为0,导致t1线程陷入死循环,程序执行不会继续下去。
解决方法
为相识决这个线程不可见性的题目,可以利用以下两种常见方法:
利用volatile关键字
:将fag声明为volatile,这样可以确保任何线程对fag的写入都会立即对其他线程可见。
利用同步机制
:利用synchronized关键字来确保对fag的读取和写入操作是安全的。
在这里,我们选择利用volatile关键字来解决这个题目。
public class Main {
public static volatile int fag = 0; // 使用volatile关键字
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (fag == 0) {
// Busy wait: 这里循环等待fag变为1
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
fag = 1; // 将fag设置为1
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程结束");
}
}
复制代码
结果
通过将fag声明为volatile,确保了对该变量的写入会使得线程t1线程能看到fag的最新值。即使线程t2在将fag改为1后,其他线程(如t1)也能及时看到这一变化,而不会出现不可见性的题目,从而避免了t1进入死循环的情况。
在程序运行结束后,您将看到"主线程结束"的输出,表明所有线程都能正常结束。利用volatile关键字有效地解决了线程间的可见性题目。
死锁
在多线程编程中,死锁是一种非常严重的题目,它会导致程序无法继续执行。产存亡锁的典范条件通常可以归纳为以下四个须要条件:
互斥条件
:至少有一个资源必须被一个线程持有,而且在该资源被其他线程哀求时,该线程不能被剥夺,即资源只能被一个线程利用。
保持并等待条件
:一个线程至少持有一个资源,而且正在等待获取其他资源。在这个状态下,线程不会释放它已持有的资源。
不剥夺条件
:一旦资源被分配给某个线程,其他线程不能强制剥夺该资源,只有线程在完成其使命后才能释放它所持有的资源。
循环等待条件
:存在一个线程聚集 {T1, T2, …, Tn},此中 T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,以此类推,直至 Tn 等待 T1 持有的资源。形成一种循环等待的关系。
死锁案例
假设有两个线程,线程A和线程B,它们分别必要获取两个锁,锁1和锁2。以下是代码示例:
class Lock {
private final String name;
public Lock(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class DeadlockExample {
private static final Lock lock1 = new Lock("Lock1");
private static final Lock lock2 = new Lock("Lock2");
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread A: Holding lock 1...");
// Simulate some work
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread A: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread A: Acquired lock 2!");
}
}
});
Thread threadB = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread B: Holding lock 2...");
// Simulate some work
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread B: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread B: Acquired lock 1!");
}
}
});
threadA.start();
threadB.start();
}
}
复制代码
分析
在上面的代码中,线程A起首持有锁1,然后实验去获取锁2。同时,线程B起首持有锁2,之后实验获取锁1。这样就形成了循环等待,导致两个线程相互阻塞,从而发存亡锁。
解决方法
为了避免这种死锁情况,可以利用以下解决方案:
按照固定次序获取锁
: 我们可以定义一个次序,确保所有线程都按照类似的次序获取锁,从而避免循环等待。
public class DeadlockPrevention {
private static final Lock lock1 = new Lock(“Lock1”);
private static final Lock lock2 = new Lock(“Lock2”);
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
Lock firstLock = lock1;
Lock secondLock = lock2;
acquireLocks(firstLock, secondLock);
});
Thread threadB = new Thread(() -> {
Lock firstLock = lock1;
Lock secondLock = lock2;
acquireLocks(firstLock, secondLock);
});
threadA.start();
threadB.start();
}
private static void acquireLocks(Lock firstLock, Lock secondLock) {
synchronized (firstLock) {
System.out.println(Thread.currentThread().getName() + ": Holding " + firstLock.getName() + "...");
// Simulate some work
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (secondLock) {
System.out.println(Thread.currentThread().getName() + ": Acquired " + secondLock.getName() + "!");
}
}
}
复制代码
}
在这个示例中,无论线程A还是线程B,都会按照同样的次序(起首获取lock1,然后是lock2)来哀求锁,由此避免了死锁情况的发生。
通过这些方法,可以有效减少多线程程序中的死锁风险,保证程序的稳定性。
为了有效避免死锁,可以考虑以下计谋:
资源有序分配
:为所有资源定义一个全局的获取次序,线程在哀求资源时,按照这个次序获取,从而避免循环等待的情况。
利用超时机制
:在实验获取锁时,可以设定一个超时时间,若超时则放弃锁的哀求,减少埋伏的死锁情况。
避免保持并等待
:可以在开始线程时一次性哀求所有所需资源,成功则继续执行,失败则释放所有已获得的资源。
检测与恢复
:定期查抄体系中是否存在死锁,如果发现可以停止某些线程大概释放某些资源来排除死锁。
通过合理的设计与计划,可以有效减少死锁的可能性,进步体系的稳定性和可靠性。
博主介绍
:大厂架构|《java全套学习资料》作者,阿里云开发社区乘风者计划专家博主,CSDN平台Java领域优质创作者,专注于大门生项目实战开发、讲解和结业答疑辅导。
主要项目
:小程序、SpringBoot、SSM、Vue、Html、Jsp、Nodejs等设计与开发。
??文末获取源码接洽??
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4