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


    目录

    一、什么是线程安全?

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

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

    四、解决方案 

    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中线程安全的类,后续补充) 

  • 相关阅读:
    Java源码阅读
    GPU架构与计算入门指南
    C++:vector 定义,用法,作用,注意点
    Navicat连接postgresql时出现‘datlastsysoid does not exist‘报错
    [CC2642r1] ble5 stacks 蓝牙协议栈 介绍和理解
    MySQL数据库忘记密码之修改密码
    kafka的安装和使用
    软考高项-写作准备&写作步骤
    在Linux中进行K8s部署
    测试环境顺便扩容记录
  • 原文地址:https://blog.csdn.net/LYJbao/article/details/126751783