• 面试遇到并发编程,爽还是酸爽?


    你好,我是田哥

    今天,一位朋友跟我反馈,在面试中被问到并发编程的特性有哪些?

    72886886bece0c646d6f46d34bd70c84.png

    可惜的是面试官没有展开问,只是随口一问,我也在想这位面试官估计也不是很看重这一块,看重的是业务处理能力(上面那位朋友反馈的是:面试官很喜欢问项目的问题)。说实话,这个问题要是全面真的展开讲,你可以和面试官聊一天,或许都聊不完。

    并发编程的三大问题,也叫三大特性,也有的叫三大因素:

    • 原子性

    • 可见性

    • 有序性

    我们本文就用代码来演示一下这三个老东西,理论形式看完很容易忘掉,所以我想通过这种代码的方式和你聊聊。

    原子性

    什么叫原子性?

    一个操作或者多个操作,要么全部执行成功,要么全部执行失败。满足原子性的操作,中途不可被中断。

    整个案例:

    1. package com.tian.utils;
    2. /**
    3.  * @author tianwc
    4.  * @version 1.0.0
    5.  * @description 原子性演示
    6.  * @公众号 Java后端技术全栈
    7.  */
    8. public class AtomDemo {
    9.     private static int a = 0;
    10.     public static void incr() {
    11.         try {
    12.             //睡一会 更好看出问题
    13.             Thread.sleep(1);
    14.         } catch (InterruptedException e) {
    15.             e.printStackTrace();
    16.         }
    17.         a++;
    18.     }
    19.     public static void main(String[] args) {
    20.         //1000个线程
    21.         for (int i = 0; i < 1000; i++) {
    22.             Thread thread = new Thread(new Runnable() {
    23.                 @Override
    24.                 public void run() {
    25.                     incr();
    26.                 }
    27.             });
    28.             thread.start();
    29.         }
    30.         try {
    31.             //主线程等10秒
    32.             Thread.sleep(10000);
    33.         } catch (InterruptedException e) {
    34.             e.printStackTrace();
    35.         }
    36.         System.out.println(a);
    37.     }
    38. }

    执行上面这段代码,你会发现输出的结果并不是固定,这就是原子性的问题。

    什么地方的原子性问题呢?

    a++

    为什么说a++就不是原子操作呢?我们可以将这个操作拆分为3个步骤。

    1、线程从主内存把遍历加载到缓存。

    2、线程执行a++操作。

    3、线程将a的新值刷新到主内存。

    如果此时两个线程来做a++,假设a初始值等于0,线程1拿到a=0,线程2此时也来了,拿到a=0,然后线程1对其进行a++,把a=1刷新到主内存中,最后线程2也对a++,也把a=1刷新到主内存中。

    也就是原本来个线程执行a++,理论结果是a=2,但a=1了。

    解决方案

    我们可以通过synchronized关键和AtomicInteger 来解决上面的原子性问题。

    synchronized解决方案:

    1. package com.tian.utils;
    2. /**
    3.  * @author tianwc
    4.  * @version 1.0.0
    5.  * @description 原子性演示
    6.  * @公众号 Java后端技术全栈
    7.  */
    8. public class AtomDemo {
    9.     private static int a = 0;
    10.     //incr()方法添加synchronized关键字的修饰
    11.     public synchronized static void incr() {
    12.         try {
    13.             Thread.sleep(1);
    14.         } catch (InterruptedException e) {
    15.             e.printStackTrace();
    16.         }
    17.         a++;
    18.     }
    19.     public static void main(String[] args) {
    20.         //10个线程
    21.         for (int i = 0; i < 1000; i++) {
    22.             Thread thread = new Thread(new Runnable() {
    23.                 @Override
    24.                 public void run() {
    25.                     incr();
    26.                 }
    27.             });
    28.             thread.start();
    29.         }
    30.         try {
    31.             Thread.sleep(5000);
    32.         } catch (InterruptedException e) {
    33.             e.printStackTrace();
    34.         }
    35.         System.out.println(a);
    36.     }
    37. }

    AtomicInteger解决方案:

    1. import java.util.concurrent.atomic.AtomicInteger;
    2. /**
    3.  * @author tianwc
    4.  * @version 1.0.0
    5.  * @description 原子性演示
    6.  * @公众号 Java后端技术全栈
    7.  */
    8. public class AtomDemo {
    9.     //初始值为0
    10.     private static AtomicInteger a = new AtomicInteger(0);
    11.     public static void incr() {
    12.         try {
    13.             Thread.sleep(1);
    14.         } catch (InterruptedException e) {
    15.             e.printStackTrace();
    16.         }
    17.         //自增+1
    18.         a.incrementAndGet();
    19.     }
    20.     public static void main(String[] args) {
    21.         //10个线程
    22.         for (int i = 0; i < 1000; i++) {
    23.             Thread thread = new Thread(new Runnable() {
    24.                 @Override
    25.                 public void run() {
    26.                     incr();
    27.                 }
    28.             });
    29.             thread.start();
    30.         }
    31.         try {
    32.             Thread.sleep(5000);
    33.         } catch (InterruptedException e) {
    34.             e.printStackTrace();
    35.         }
    36.         System.out.println(a);
    37.     }
    38. }

    到这里,关于原子性问题应该不难理解了吧。

    另外,很多人对于volatile关键字不能保证原子性的问题,也是不太理解。

    我们在回到上面的案例中,如果我们没有使用到synchronizedAtomicInteger来解决,使用volatile来试试。

    1. /**
    2.  * @author tianwc
    3.  * @version 1.0.0
    4.  * @description 原子性演示
    5.  * @公众号 Java后端技术全栈
    6.  */
    7. public class AtomDemo {
    8.     //使用volatile
    9.     private volatile static int a = 0;
    10.     public static void incr() {
    11.         try {
    12.             Thread.sleep(1);
    13.         } catch (InterruptedException e) {
    14.             e.printStackTrace();
    15.         }
    16.         a++;
    17.     }
    18.     public static void main(String[] args) {
    19.         //10个线程
    20.         for (int i = 0; i < 1000; i++) {
    21.             Thread thread = new Thread(new Runnable() {
    22.                 @Override
    23.                 public void run() {
    24.                     incr();
    25.                 }
    26.             });
    27.             thread.start();
    28.         }
    29.         try {
    30.             Thread.sleep(5000);
    31.         } catch (InterruptedException e) {
    32.             e.printStackTrace();
    33.         }
    34.         System.out.println(a);
    35.     }
    36. }

    运行结果可能是1000,但只要你多次运行上面这段代码,你会发现结可能会少于1000。所以,volatile并不能保证i++之类的原子性。

    可见性

    什么叫可见性?多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。

    整个案例:

    1. /**
    2.  * @author tianwc
    3.  * @version 1.0.0
    4.  * @description 可见性演示
    5.  * @公众号 Java后端技术全栈
    6.  */
    7. public class VisibleDemo {
    8.     private static boolean flag = false;
    9.     public static void main(String[] args) {
    10.         Thread thread = new Thread(new Runnable() {
    11.             @Override
    12.             public void run() {
    13.                 int i = 0;
    14.                 while (!flag) {
    15.                     i++;
    16.                 }
    17.                 System.out.println(i);
    18.             }
    19.         });
    20.         thread.start();
    21.         try {
    22.             Thread.sleep(1000);
    23.         } catch (InterruptedException e) {
    24.             e.printStackTrace();
    25.         }
    26.         flag = true;
    27.     }
    28. }

    上面这段代码的逻辑很简单,定义一个静态变量flag。main方法中创建一个子线程,子线程中通过判断flag的值来对i进行++操作,最后输出,启动子线程。主线程先睡1000毫秒,然后把flag设置为true。

    主线程和子线程共享变量flag,可见性就是主线程把flag改成true后,子线程能不能看到。

    上面的这段代码,运行结果是子线程一直死循环,没有任何输出。

    4f3cca3934307d929141962a4bca8da4.png


    解决方案

    最直观的解决方案就是给flag变量加上volatile关键字修饰。

    1. /**
    2.  * @author tianwc
    3.  * @version 1.0.0
    4.  * @description 可见性演示
    5.  * @公众号 Java后端技术全栈
    6.  */
    7. public class VisibleDemo {
    8.     private volatile static boolean flag = false;
    9.     public static void main(String[] args) {
    10.         Thread thread = new Thread(new Runnable() {
    11.             @Override
    12.             public void run() {
    13.                 int i = 0;
    14.                 while (!flag) {
    15.                     i++;
    16.                 }
    17.                 System.out.println(i);
    18.             }
    19.         });
    20.         thread.start();
    21.         try {
    22.             Thread.sleep(1000);
    23.         } catch (InterruptedException e) {
    24.             e.printStackTrace();
    25.         }
    26.         flag = true;
    27.     }
    28. }

    再次运行上面这段代码:

    9c80ebaf1cb9a274c62937783779fa56.png


    正常的输出结果了,说明while判断中flag已经变成了true,也就是主线程对flag的修改,子线程是可以知道的啦。

    田哥在写这个案例的时候,发现一丢丢问题,不知道你会怎么想,请看问题:

    1. public class VisibleDemo {
    2.     //没有volatile关键字修饰
    3.     private  static boolean flag = false;
    4.     public static void main(String[] args) {
    5.         Thread thread = new Thread(new Runnable() {
    6.             @Override
    7.             public void run() {
    8.                 int i = 0;
    9.                 while (!flag) {
    10.                     i++;
    11.                     System.out.println(i);//把输出提到while里
    12.                 }
    13.             }
    14.         });
    15.         thread.start();
    16.         try {
    17.             Thread.sleep(1000);
    18.         } catch (InterruptedException e) {
    19.             e.printStackTrace();
    20.         }
    21.         flag = true;
    22.     }
    23. }

    你觉得上面这段代码运行结果是怎样的?

    再来一个问题,请看代码:

    1. public class VisibleDemo {
    2.     private  static boolean flag = false;
    3.     public static void main(String[] args) {
    4.         Thread thread = new Thread(new Runnable() {
    5.             @Override
    6.             public void run() {
    7.                 int i = 0;
    8.                 while (!flag) {
    9.                     i++;
    10.                     //在while添加Thread.sleep()
    11.                     try {
    12.                         Thread.sleep(1);
    13.                     } catch (InterruptedException e) {
    14.                         e.printStackTrace();
    15.                     }
    16.                 }
    17.                 System.out.println(i);
    18.             }
    19.         });
    20.         thread.start();
    21.         try {
    22.             Thread.sleep(1000);
    23.         } catch (InterruptedException e) {
    24.             e.printStackTrace();
    25.         }
    26.         flag = true;
    27.     }
    28. }

    这段代码运行结果又是怎么样的?

    不绕弯子了,这里两种都会运行结束。

    3a77e8edca4194b174c5a9e98f06fb36.png


    333494b8850753b7fd698921364a7366.png


    有序性

    什么是有序性?程序执行的顺序按照代码的先后顺序执行。

    (由于JMM模型中允许编译器和处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性)

    整个案例:

    1. int num = 0;
    2. boolean ready = false;
    3. // 线程1 执行此方法
    4. public void action1(I_Result r) {
    5.     if(ready) {
    6.         r.r1 = num + num;
    7.     } else {
    8.         r.r1 = 1;
    9.     }
    10. }
    11. // 线程2 执行此方法
    12. public void action2(I_Result r) {
    13.     num = 2;//
    14.     ready = true;//
    15. }

    上面这段代码,最终r.r1结果是多少?

    下面我们来分析一下:

    情况1:线程1 先执行,此时 ready = false,所有进入else ,结果为1

    情况2:线程2 先执行 num = 2,但还没来得及执行 ready = true,线程1 开始执行,还是进入else ,结果为1

    情况3:线程2 先执行到ready = true,线程1 执行,进入else ,结果为4

    情况4:由于指令重排序,线程2先执行 ready = true。切换到线程1,进入 if 分支,相加为0,再切回线程2,执行 num = 2,结果为0。

    我们在来看一个案例:单例模式

    单列模式的写法有很多种,我之前也分享过:

    一个单例模式,被问7个问题,难!

    这里我们说的单例模式是双重检查锁单例模式。

    1. public class SingleLazy {
    2.     private SingleLazy() {}
    3.     private  static SingleLazy INSTANCE;
    4.     // 获取实体
    5.     public static SingleLazy getInstance() {
    6.         // 实例未被创建,开启同步代码块准备创建
    7.         if (INSTANCE == null) {
    8.             synchronized (SingleLazy.class) {
    9.                 // 也许其他线程在判断完后已经创建,再次判断
    10.                 if (INSTANCE == null) {
    11.                     INSTANCE = new SingleLazy();
    12.                 }
    13.             }
    14.         }
    15.         return INSTANCE;
    16.     }
    17. }

    看下程序运行流程:

    • 假设两个线程 t1、t2 同时调用 getInstance(),同时发现INSTANCE == null;

    • 于是同时对 SingleLazy.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 t1),另外一个线程则会处于等待状态(假设是线程 t2);

    • 线程 t1 会创建一个 SingleLazy 实例,之后释放锁,锁释放后,线程 t2 被唤醒,线程t2 再次尝试加锁,此时是可以加锁成功的;

    • t2 加锁成功后,线程 t2 再次检查 INSTANCE == null 是否成立,发现 INSTANCE!=null,不会再去实例化INSTANCE,直接返回线程t1实例化的INSTANCE。

    有没有觉得这不挺好的么?怎么扯到有序性这里来了。

    创建一个对象其实需要三步:

    1. memory = allocate();//1.分配对象内存空间
    2. instance(memory);//2.初始化对象
    3. instance = memory;//3.设置instance指向刚分配的内存地址,此时instance e != null

    其中第一步和第二步可能会发生指令重排导致安全性问题:

    如果发生重排序,会导致什么问题呢?

    依然假设线程 t1 先去执行 getInstance() 方法(和线程t2并不是同时去执行),直接获取到锁,并执行实例化new操作,当执行完指令 2 时恰好发生了线程切换,切换到了线程 t2 上;如果此时线程 t2 也执行 getInstance() 方法,那么线程 t2 在执行第一个判断时会发现 INSTANCE!= null ,所以直接返回 INSTANCE,而此时的 INSTANCE是没有初始化过的,如果我们这个时候访问 INSTANCE的成员变量就可能触发空指针异常。

    这类重排序也成为编译优化带来的有序性问题

    解决方案

    1. public class SingleLazy {
    2.     private SingleLazy() {}
    3.     //添加volatile
    4.     private volatile static SingleLazy INSTANCE;
    5.     // 获取实体
    6.     public static SingleLazy getInstance() {
    7.         // 实例未被创建,开启同步代码块准备创建
    8.         if (INSTANCE == null) {
    9.             synchronized (SingleLazy.class) {
    10.                 // 也许其他线程在判断完后已经创建,再次判断
    11.                 if (INSTANCE == null) {
    12.                     INSTANCE = new SingleLazy();
    13.                 }
    14.             }
    15.         }
    16.         return INSTANCE;
    17.     }
    18. }

    此时,我们只需要添加volatile修饰INSTANCE即可解决重排序带来的问题。

    注意: JDK1.5前的 volatile 关键字不保证指令重排问题

    如果面试官问volatile,记得把上面这个JDK版本的提出来说一下,不然怎么显得你比别人知道的多呢1845b25e22f98cf13a0ab21a780a1cd6.png

    好了,关于并发编程的三大问题就聊这些。本文只是为了演示,所以没有做太深入的讲解,这里面会涉及到Java内存模型,还会涉及到JVM源码。

    我之前画过一个《并发编程核心知识》思维导图;

    41390142e7312905dad435efb1f3c0a7.png


    思维导图可以让你构建自己的知识学习路径,学完后很多技术可能不怎么用,很快就容易忘掉,思维导图就是最好的快速复习资料。

    免费获取并发编程核心知识思维导图,请加我加我微信:tj20120622,备注“并发”。

    到目前为止,我已经花了快30张的思维导图。

    8cfeaa365572a86b5ba4ab17ebb09e79.png

    9d99c08f4904be63a5fb9636c1d23090.png

    另外,也在不断更新《Java后端核心知识总结》;

    11aa5f1be82bf474ca3706e466af6fbf.png

    《面试小抄》第三版已完结,现已开始着手准备第四版。

    《面试小抄》第三版

    《Java学习专栏》已更新771篇文章:

    《Java学习专栏》最新版

    加入我的知识星球,即可获取上述所有资料,其实还有很多:

    9542f41562274120e9a966a7eb7665b1.png

    29f1e3a0b6bd28b9d96701528cd6cacd.png

    35d001183ac1ed0696240e1769d345d7.png

    bd1503213fe7fb2599b32a03a936ee69.png

    97d8b09d8bfa3c99725d42e0bbc69ba3.png

    9a153bb92e967618da71ef06a7c8697f.png

    5803dd58f1e84ff38fce0fd184cebf61.png

    .......

    记得加我微信,微信好友,我都给惊喜价。

  • 相关阅读:
    【力扣每日一题】2023.9.21 收集树中金币
    C++ Qt开发:CheckBox多选框组件
    【电商】管理后台篇之安全、菜单、通知管理
    JAVA集合问答
    第160场直播带货数据分享
    IO作业:文件IO、fork,用父子进程拷贝一张图片,用文件IO实现,要求 子进程拷贝后半部分,父进程拷贝前半部分,按照cpu调度机制同时执行
    购买油封时必须了解的步骤
    【应用统计学】参数统计-抽样分布
    springboot配置过滤器和多个拦截器、执行顺序
    ElementUI用el-table实现表格内嵌套表格
  • 原文地址:https://blog.csdn.net/o9109003234/article/details/126313914