【JavaEE】线程安全性题目,线程不安满是怎么产生的,该如何应对 ...

打印 上一主题 下一主题

主题 846|帖子 846|积分 2538

产生线程不安全的缘故原由

在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见缘故原由:

  • 共享可变状态:当多个线程对共享的可变数据进行读写时,假如没有得当的同步机制,大概导致数据的不一致性。比方,两个线程同时修改一个共享变量,终极的结果大概取决于线程的执行顺序。
  • 缺乏同步:在没有使用synchronized关键字或其他同步机制(如Lock)进行保护的情况下,多个线程可以同时进入临界区,从而导致线程安全题目。
  • 指令重排序:为了进步执行效率,Java虚拟机和处理器大概会对指令进行重排序,这种行为在多线程环境中大概导致不可预期的结果,尤其是在多个线程依赖某些变量的状态时。
  • 原子性题目:某些利用在Java中并不是原子的,比方对对象属性的读-改-写利用。在多线程环境下,这类利用必须通过同步处理以确保原子性。
  • 死锁:尽管死锁本身不直接导致线程不安全,但在复杂的同步情况下,死锁大概导致某些线程无法继续执行,从而影响整体程序的正确性与稳定性。
  • 不可见性:当一个线程对共享变量的修改在其他线程中不可见时,大概导致一些线程读取到过时的值。这通常可以通过使用volatile关键字来解决。

产生线程不安全的案例以及应对方法

共享可变状态案例

我们将创建一个简单的银行账户类,多个线程并发访问该账户进行存款和取款利用。假设我们有两个线程同时对账户进行利用,大概会出现余额盘算错误的情况。
  1. class BankAccount {
  2.     private int balance = 100; // 初始余额为100
  3.     public void deposit(int amount) {
  4.         balance += amount; // 存款
  5.     }
  6.     public void withdraw(int amount) {
  7.         balance -= amount; // 取款
  8.     }
  9.     public int getBalance() {
  10.         return balance; // 返回当前余额
  11.     }
  12. }
  13. public class UnsafeBank {
  14.     public static void main(String[] args) {
  15.         BankAccount account = new BankAccount();
  16.         // 创建两个线程同时操作
  17.         Thread t1 = new Thread(() -> {
  18.             account.withdraw(50);
  19.             System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());
  20.         });
  21.         Thread t2 = new Thread(() -> {
  22.             account.deposit(30);
  23.             System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());
  24.         });
  25.         t1.start();
  26.         t2.start();
  27.     }
  28. }
复制代码
运行情况: 

我们期望的运行结果是:取款50,余额50、存款30,余额80。但是上述结果并不是我们想要的
分析
在上述代码中,两个线程同时对balance变量进行利用,大概导致不一致的余额输出。比方,假设Thread 1先读取了余额为100,然后进行了取款利用,但在它更新余额之前,Thread 2大概已经读取了余额并进行了存款利用。终极的结果大概不符合预期。
解决方法

为相识决这个线程不安全的题目,我们可以使用synchronized关键字来确保对共享资源的访问是线程安全的。我们可以对deposit和withdraw方法加锁,使得同一时间只有一个线程能够执行此中一个方法。
以下是修改后的代码:
  1. class BankAccount {
  2.     private int balance = 100; // 初始余额为100
  3.     // 存款操作
  4.     public synchronized void deposit(int amount) {
  5.         balance += amount; // 存款
  6.     }
  7.     // 取款操作
  8.     public synchronized void withdraw(int amount) {
  9.         balance -= amount; // 取款
  10.     }
  11.     // 返回当前余额
  12.     public int getBalance() {
  13.         return balance; // 返回当前余额
  14.     }
  15. }
  16. public class SafeBank {
  17.     public static void main(String[] args) throws InterruptedException {
  18.         BankAccount account = new BankAccount();
  19.         // 创建两个线程同时操作
  20.         Thread t1 = new Thread(() -> {
  21.             account.withdraw(50);
  22.             System.out.println("Thread 1 withdrew 50, balance: " + account.getBalance());
  23.         });
  24.         Thread t2 = new Thread(() -> {
  25.             account.deposit(30);
  26.             System.out.println("Thread 2 deposited 30, balance: " + account.getBalance());
  27.         });
  28.         t1.start();
  29.         t2.start();
  30.         // 等待两个线程结束
  31.         t1.join();
  32.         t2.join();
  33.         
  34.         // 输出最终余额
  35.         System.out.println("Final balance: " + account.getBalance());
  36.     }
  37. }
复制代码
结果
在修改后的代码中,由于对deposit和withdraw方法加了synchronized修饰,确保任何时候只有一个线程可以执行这两个方法,从而避免了由于竞争条件导致的不一致性。终极输出的余额将与预期结果相一致。



指令重排序案例

指令重排序是指在编译、优化或CPU执行过程中,代码的执行顺序被改变。
count++ 利用并不是一个原子利用,它是由三个步骤组成的:

  • 读取当前的值。
  • 对值加1。
  • 将新值写回。
在多线程环境中,多个线程大概会同时对同一变量进行 count++ 利用,导致结果不正确。这种情况下,指令重排序大概导致某些利用无法达到预期结果。
以下是一个示例代码,演示了这个题目:
  1. class Counter {
  2.     private int count = 0;
  3.     public void increment() {
  4.         count++; // 不安全的操作
  5.     }
  6.     public int getCount() {
  7.         return count;
  8.     }
  9. }
  10. public class CountExample {
  11.     public static void main(String[] args) throws InterruptedException {
  12.         Counter counter = new Counter();
  13.         
  14.         Thread[] threads = new Thread[10];
  15.         
  16.         // 创建10个线程
  17.         for (int i = 0; i < 10; i++) {
  18.             threads[i] = new Thread(() -> {
  19.                 for (int j = 0; j < 1000; j++) {
  20.                     counter.increment(); // 增加计数
  21.                 }
  22.             });
  23.         }
  24.         
  25.         // 启动所有线程
  26.         for (Thread thread : threads) {
  27.             thread.start();
  28.         }
  29.         // 等待所有线程结束
  30.         for (Thread thread : threads) {
  31.             thread.join();
  32.         }
  33.         // 输出最终计数
  34.         System.out.println("Final count: " + counter.getCount());
  35.     }
  36. }
复制代码
运行结果: 

我们的预期结果是:10000
分析
在上述代码中,我们创建了10个线程,每个线程执行1000次 increment() 方法,从而期望终极的计数是10000。然而,由于 count++ 利用的非原子性,在多个线程并发执行时,大概会导致某些增量利用丢失,终极结果大概小于10000。
解决方法

为相识决这个题目,可以使用以下几种方法:

  • 使用synchronized关键字:将increment方法同步,以确保同一时候只有一个线程能执行该利用。
  • **使用AtomicInteger**:Java提供了原子类AtomicInteger,能够包管对整数利用的原子性。
我们将接纳第二种方法,即使用 AtomicInteger 来解决这个题目。
以下是修改后的代码:
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. class Counter {
  3.     private AtomicInteger count = new AtomicInteger(0); // 使用AtomicInteger
  4.     public void increment() {
  5.         count.incrementAndGet(); // 原子性增加
  6.     }
  7.     public int getCount() {
  8.         return count.get(); // 获取当前值
  9.     }
  10. }
  11. public class SafeCountExample {
  12.     public static void main(String[] args) throws InterruptedException {
  13.         Counter counter = new Counter();
  14.         
  15.         Thread[] threads = new Thread[10];
  16.         
  17.         // 创建10个线程
  18.         for (int i = 0; i < 10; i++) {
  19.             threads[i] = new Thread(() -> {
  20.                 for (int j = 0; j < 1000; j++) {
  21.                     counter.increment(); // 增加计数
  22.                 }
  23.             });
  24.         }
  25.         
  26.         // 启动所有线程
  27.         for (Thread thread : threads) {
  28.             thread.start();
  29.         }
  30.         // 等待所有线程结束
  31.         for (Thread thread : threads) {
  32.             thread.join();
  33.         }
  34.         // 输出最终计数
  35.         System.out.println("Final count: " + counter.getCount());
  36.     }
  37. }
复制代码

不可见性案例 

我们使用了两个线程 t1 和 t2。线程 t1 负责不停地检查一个共享变量 fag,而线程 t2 则在休眠1秒后将 fag 设为1。
  1. public class Main {
  2.     public static int fag = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1 = new Thread(() -> {
  5.             while (fag == 0) {
  6.             }
  7.         });
  8.         Thread t2 = new Thread(() -> {
  9.             try {
  10.                 Thread.sleep(1000);
  11.             } catch (InterruptedException e) {
  12.                 throw new RuntimeException(e);
  13.             }
  14.             fag = 1;
  15.         });
  16.         t1.start();
  17.         t2.start();
  18.         t1.join();
  19.         t2.join();
  20.         System.out.println("主线程结束");
  21.     }
  22. }
复制代码

 分析

在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 关键字来解决这个题目。
  1. public class Main {
  2.     public static volatile int fag = 0; // 使用volatile关键字
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1 = new Thread(() -> {
  5.             while (fag == 0) {
  6.                 // Busy wait: 这里循环等待fag变为1
  7.             }
  8.         });
  9.         Thread t2 = new Thread(() -> {
  10.             try {
  11.                 Thread.sleep(1000);
  12.             } catch (InterruptedException e) {
  13.                 throw new RuntimeException(e);
  14.             }
  15.             fag = 1; // 将fag设置为1
  16.         });
  17.         t1.start();
  18.         t2.start();
  19.         t1.join();
  20.         t2.join();
  21.         System.out.println("主线程结束");
  22.     }
  23. }
复制代码
结果
通过将 fag 声明为 volatile,确保了对该变量的写入会使得线程 t1 线程能看到 fag 的最新值。即使线程 t2 在将 fag 改为1后,其他线程(如 t1)也能实时看到这一变革,而不会出现不可见性的题目,从而避免了 t1 进入死循环的情况。
在程序运行竣事后,您将看到"主线程竣事"的输出,表明全部线程都能正常竣事。使用 volatile 关键字有用地解决了线程间的可见性题目。

 死锁

在多线程编程中,死锁是一种非常严肃的题目,它会导致程序无法继续执行。产生死锁的典型条件通常可以归纳为以下四个必要条件:

  • 互斥条件:至少有一个资源必须被一个线程持有,并且在该资源被其他线程请求时,该线程不能被剥夺,即资源只能被一个线程使用。
  • 保持并等待条件:一个线程至少持有一个资源,并且正在等待获取其他资源。在这个状态下,线程不会释放它已持有的资源。
  • 不剥夺条件:一旦资源被分配给某个线程,其他线程不能强制剥夺该资源,只有线程在完成其任务后才能释放它所持有的资源。
  • 循环等待条件:存在一个线程聚集 {T1, T2, ..., Tn},此中 T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,以此类推,直至 Tn 等待 T1 持有的资源。形成一种循环等待的关系。
死锁案例

假设有两个线程,线程A和线程B,它们分别需要获取两个锁,锁1和锁2。以下是代码示例:
  1. class Lock {
  2.     private final String name;
  3.     public Lock(String name) {
  4.         this.name = name;
  5.     }
  6.     public String getName() {
  7.         return name;
  8.     }
  9. }
  10. public class DeadlockExample {
  11.     private static final Lock lock1 = new Lock("Lock1");
  12.     private static final Lock lock2 = new Lock("Lock2");
  13.     public static void main(String[] args) {
  14.         Thread threadA = new Thread(() -> {
  15.             synchronized (lock1) {
  16.                 System.out.println("Thread A: Holding lock 1...");
  17.                
  18.                 // Simulate some work
  19.                 try { Thread.sleep(100); } catch (InterruptedException e) {}
  20.                 System.out.println("Thread A: Waiting for lock 2...");
  21.                 synchronized (lock2) {
  22.                     System.out.println("Thread A: Acquired lock 2!");
  23.                 }
  24.             }
  25.         });
  26.         Thread threadB = new Thread(() -> {
  27.             synchronized (lock2) {
  28.                 System.out.println("Thread B: Holding lock 2...");
  29.                
  30.                 // Simulate some work
  31.                 try { Thread.sleep(100); } catch (InterruptedException e) {}
  32.                 System.out.println("Thread B: Waiting for lock 1...");
  33.                 synchronized (lock1) {
  34.                     System.out.println("Thread B: Acquired lock 1!");
  35.                 }
  36.             }
  37.         });
  38.         threadA.start();
  39.         threadB.start();
  40.     }
  41. }
复制代码
分析
在上面的代码中,线程A首先持有锁1,然后尝试去获取锁2。同时,线程B首先持有锁2,之后尝试获取锁1。如许就形成了循环等待,导致两个线程相互阻塞,从而发生死锁。
解决方法

为了避免这种死锁情况,可以使用以下解决方案:

  • 按照固定顺序获取锁: 我们可以定义一个顺序,确保全部线程都按照类似的顺序获取锁,从而避免循环等待。
  1. public class DeadlockPrevention {
  2.     private static final Lock lock1 = new Lock("Lock1");
  3.     private static final Lock lock2 = new Lock("Lock2");
  4.     public static void main(String[] args) {
  5.         Thread threadA = new Thread(() -> {
  6.             Lock firstLock = lock1;
  7.             Lock secondLock = lock2;
  8.             acquireLocks(firstLock, secondLock);
  9.         });
  10.         Thread threadB = new Thread(() -> {
  11.             Lock firstLock = lock1;
  12.             Lock secondLock = lock2;
  13.             acquireLocks(firstLock, secondLock);
  14.         });
  15.         threadA.start();
  16.         threadB.start();
  17.     }
  18.     private static void acquireLocks(Lock firstLock, Lock secondLock) {
  19.         synchronized (firstLock) {
  20.             System.out.println(Thread.currentThread().getName() + ": Holding " + firstLock.getName() + "...");
  21.             // Simulate some work
  22.             try { Thread.sleep(100); } catch (InterruptedException e) {}
  23.             synchronized (secondLock) {
  24.                 System.out.println(Thread.currentThread().getName() + ": Acquired " + secondLock.getName() + "!");
  25.             }
  26.         }
  27.     }
  28. }
复制代码
在这个示例中,无论线程A还是线程B,都会按照同样的顺序(首先获取lock1,然后是lock2)来请求锁,由此避免了死锁情况的发生。
通过这些方法,可以有用淘汰多线程程序中的死锁风险,包管程序的稳定性。

为了有用避免死锁,可以思量以下战略:


  • 资源有序分配:为全部资源定义一个全局的获取顺序,线程在请求资源时,按照这个顺序获取,从而避免循环等待的情况。
  • 使用超时机制:在尝试获取锁时,可以设定一个超时时间,若超时则放弃锁的请求,淘汰潜在的死锁情况。
  • 避免保持并等待:可以在开始线程时一次性请责备部所需资源,成功则继续执行,失败则释放全部已获得的资源。
  • 检测与恢复:定期检查系统中是否存在死锁,假如发现可以中断某些线程大概释放某些资源来解除死锁。
通过合理的计划与计划,可以有用淘汰死锁的大概性,进步系统的稳定性和可靠性。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

忿忿的泥巴坨

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表