当前位置:   article > 正文

总结线程安全问题的原因和解决方案_多线程线程安全问题

多线程线程安全问题

目录

一、什么是线程安全?

二、线程不安全带来的问题举例:

三、线程不安全的原因总结:

四、解决方案 

1、同步代码块

2、同步方法

3、静态同步方法

4、加锁Lock解决问题

五、面试提问:为什么会有线程不安全的问题出现?


一、什么是线程安全?

        在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的

二、线程不安全带来的问题举例:

售票问题:

  1. public class Test {
  2. private static int ticketCount=1;
  3. public static void main(String[] args) {
  4. //t1模拟售票窗口一
  5. Thread t1= new Thread(()-> {
  6. while(ticketCount<100) {
  7. System.out.println((Thread.currentThread().getName()+"正在卖第:"+ticketCount++ +"张票"));
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. throw new RuntimeException(e);
  12. }
  13. }
  14. });
  15. //t2模拟售票窗口二
  16. Thread t2 = new Thread(()-> {
  17. while(ticketCount<100) {
  18. System.out.println((Thread.currentThread().getName()+"正在卖第:"+ticketCount++ +"张票"));
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. throw new RuntimeException(e);
  23. }
  24. }
  25. });
  26. t1.setName("窗口1");
  27. t2.setName("窗口2");
  28. t1.start();
  29. t2.start();
  30. }
  31. }

来看输出结果所出现的问题:

         看标红的地方,出现了两个窗口售卖同一张票的情况,这就是多线程所导致的线程安全问题

三、线程不安全的原因总结:

1、抢占式执行

————多个线程的调度执行过程,可以视为是“全随机”的

2、多个线程修改同一个变量

3、修改操作不是原子的

原子性:

         定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

        原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。

(2)所有引用reference的赋值操作

(3)java.concurrent.Atomic.* 包中所有类的一切操作

4、内存可见性问题

可见性:

        定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

        在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

5、指令重排序

有序性:
        定义:即程序执行的顺序按照代码的先后顺序执行。

        Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

        在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

四、解决方案 

        通常我们使用同步(关键字为synchronized)来解决这种由于多线程同时操作共享数据带来的线程安全问题。
        同步可以理解为:我们将多条操作共享数据的语句代码包成一个整体,让某个线程执行时其他线程不能执行。
        同步方案包括三种方式,它们对应的锁对象是不一样的。另外我们可以通过加锁来同步代码块,解决安全问题。
因此常用的解决方案有四种。
注意:
   同步可以解决问题的根本原因就在于锁对象上,因此要避免线程安全问题,多个线程必须使用同一个锁对象,否则,不能解决问题

1、同步代码块

格式:synchronized(对象) {
                需要被同步的代码;
            }

这里的锁对象可以是任意对象

利用该方法优化后如下:

  1. public class Test1 {
  2. private static int ticketCount=1;
  3. private static Object object= new Object();
  4. public static void main(String[] args) {
  5. //t1模拟售票窗口一
  6. Thread t1= new Thread(()-> {
  7. while(ticketCount<100) {
  8. synchronized (object) {
  9. System.out.println((Thread.currentThread().getName() + "正在卖第:" + ticketCount++ + "张票"));
  10. try {
  11. Thread.sleep(1000);
  12. } catch (InterruptedException e) {
  13. throw new RuntimeException(e);
  14. }
  15. }
  16. }
  17. });
  18. //t2模拟售票窗口二
  19. Thread t2 = new Thread(()-> {
  20. while(ticketCount<100) {
  21. synchronized (object) {
  22. System.out.println((Thread.currentThread().getName() + "正在卖第:" + ticketCount++ + "张票"));
  23. try {
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. throw new RuntimeException(e);
  27. }
  28. }
  29. }
  30. });
  31. t1.setName("窗口1");
  32. t2.setName("窗口2");
  33. t1.start();
  34. t2.start();
  35. }
  36. }

2、同步方法

 格式:把同步(synchronized)加在方法上。

这时的锁对象是this

 利用该方法优化后如下:

  1. public class Test2 {
  2. private static int ticketCount=1;
  3. public static void main(String[] args) {
  4. //t1模拟售票窗口一
  5. Thread t1= new Thread(()-> {
  6. while(ticketCount<100) {
  7. sellTicket();
  8. }
  9. });
  10. //t2模拟售票窗口二
  11. Thread t2 = new Thread(()-> {
  12. while(ticketCount<100) {
  13. sellTicket();
  14. }
  15. });
  16. t1.setName("窗口1");
  17. t2.setName("窗口2");
  18. t1.start();
  19. t2.start();
  20. }
  21. public static synchronized void sellTicket() {
  22. System.out.println((Thread.currentThread().getName()+"正在卖第:"+ticketCount++ +"张票"));
  23. try {
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. throw new RuntimeException(e);
  27. }
  28. }
  29. }

3、静态同步方法

 格式:将同步加在静态方法上

此时的锁对象为当前类的字节码文件对象

  1. public class Test3 {
  2. private static int ticketCount=1;
  3. public static void main(String[] args) {
  4. //t1模拟售票窗口一
  5. Thread t1= new Thread(()-> {
  6. while(ticketCount<100) {
  7. //同步代码块实现同步.这里设置的锁对象是该类的字节码文件对象
  8. synchronized (Test3.class) {
  9. sellTicket3();
  10. }
  11. }
  12. });
  13. Thread t2 = new Thread(()-> {
  14. while(ticketCount<100) {
  15. synchronized (Test3.class) {
  16. sellTicket3();
  17. }
  18. }
  19. });
  20. t1.setName("窗口1");
  21. t2.setName("窗口2");
  22. t1.start();
  23. t2.start();
  24. }
  25. public static synchronized void sellTicket3 () {
  26. System.out.println((Thread.currentThread().getName() + "正在卖第:" + ticketCount++ + "张票"));
  27. try {
  28. Thread.sleep(1000);
  29. } catch (InterruptedException e) {
  30. throw new RuntimeException(e);
  31. }
  32. }
  33. }

4、加锁Lock解决问题

要用lock和unlock包裹起来才能保证线程安全

  1. public class Test4 {
  2. private static int ticketCount=1;
  3. private static Lock lock= new ReentrantLock();
  4. public static void main(String[] args) {
  5. //t1模拟售票窗口一
  6. Thread t1= new Thread(()-> {
  7. while(ticketCount<100) {
  8. lock.lock();
  9. System.out.println((Thread.currentThread().getName()+"正在卖第:"+ticketCount++ +"张票"));
  10. try {
  11. Thread.sleep(100);
  12. } catch (InterruptedException e) {
  13. throw new RuntimeException(e);
  14. }
  15. lock.unlock();
  16. }
  17. });
  18. Thread t2 = new Thread(()-> {
  19. while(ticketCount<100) {
  20. lock.lock();
  21. System.out.println((Thread.currentThread().getName()+"正在卖第:"+ticketCount++ +"张票"));
  22. try {
  23. Thread.sleep(100);
  24. } catch (InterruptedException e) {
  25. throw new RuntimeException(e);
  26. }
  27. lock.unlock();
  28. }
  29. });
  30. t1.setName("窗口1");
  31. t2.setName("窗口2");
  32. t1.start();
  33. t2.start();
  34. }
  35. }

本期到这了先,下期见!!!(关于Java中线程安全的类,后续补充) 

五、面试提问:为什么会有线程不安全的问题出现?

        “线程不安全的问题主要出现在多线程环境中,当一个或多个线程在没有适当同步的情况下,同时访问共享资源或数据时,就可能引发线程不安全的问题。这是因为每个线程都有自己的执行路径和速度,它们可能同时读写同一个变量或对象的状态,导致数据的不一致或不可预测的行为。

具体来说,线程不安全的问题可能由以下几个原因引起:【主要原因总结其实就是上面的目录三】

  1. 抢占式执行

  2. 多个线程修改一个变量(数据竞争):当两个或更多线程同时访问一个数据项,并且至少有一个线程在写这个数据项时,就可能发生数据竞争。没有正确的同步,线程间的操作顺序是不确定的,这可能导致不可预见的结果。

  3. 原子性问题:某些操作,如自增或自减,看似简单,但实际上是由多个步骤组成的。如果没有原子性保证,一个线程可能在执行这些步骤时被另一个线程打断,导致操作不完整或结果不正确。

  4. 可见性问题:由于每个线程都有自己的工作内存,如果一个线程修改了一个变量的值,而其他线程没有及时看到这个修改,就会产生可见性问题。这可能导致线程读取到过时或不一致的数据。【用Java的术语来说,应该是:站在JMM的角度看待volatile,正常的程序执行过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理,编译器优化可能会导致不是每次都真的去读取主内存,而直接读取工作内存中的缓存数据(可能导致内存可见性问题),而volatile起到的效果,就是保证每次读取内存都是真的从主存中重新读取】

  5. 指令重排序(顺序问题):即使单个线程中的操作是有序的,但多线程环境中,线程之间的操作顺序可能会变得不确定。这可能导致依赖于特定操作顺序的代码出现错误。

        为了避免线程不安全的问题,我们通常需要使用同步机制,如synchronized关键字、Lock接口的实现类、volatile关键字以及并发工具类(如CountDownLatchCyclicBarrierSemaphore等)来确保线程安全。此外,还可以使用并发集合类来避免在集合操作上的线程安全问题。

        在Java中,我们还可以通过学习Java的内存模型(JMM)和Happens-Before规则来深入理解线程安全问题,并学会如何编写线程安全的代码。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/708254
推荐阅读
相关标签
  

闽ICP备14008679号