哭了,新来的实习生连并发编程死锁都不会,天天在学校混日子吧?

前言

并发编程的目的是为了提高程序的执行速度.但是并不意味着启动更多的线程会达到更好的并发效果,并发编程还会引起死锁 , 上下文频繁切换 , 线程不安全等问题.

最近我就面试了一位实习生,我叫他给我说一下并发编程死锁怎么处理,这种常规操作,他经常没有用过,因此,我准备结合工作经验,整理汇集出了并发编程实战的十八般武艺,助大家闯荡 Java 江湖一臂之力。在这里插入图片描述

并发编程的三个核心问题:

  • 分工 : 高效的拆解任务分给线程
  • 同步 : 线程之间的协作
  • 互斥 : 保证同一时刻只允许一个线程访问共享资源

学习攻略:

  • 跳出来,看全景
  • 钻进去,看本质

核心: 分工(拆分) - 同步(一个线程执行完成如何通知后续任务的线程开始工作) - 互斥(同一时刻,只允许一个线程访问共享变量)

全景:
在这里插入图片描述

本质 : 知其然知其所以然,有理论做基础.技术的本质是背后的理论模型

并发编程问题的源头

  1. 缓存导致的可见性: 对于单CPU来说,缓存是可见的,也就是说多个线程同时操作,CPU会从内存读取数据,线程更新数据到CPU,CPU写入内存,线程和CPU进行交互,这个操作每个线程之间是可见的.
    但是对于多CPU来说,多个线程操作不同的CPU,不同的CPU操作同一个内存,这会导致操作的不可见性,就出现了问题.(说下可见性的概念: 一个线程对共享变量的修改,另一个线程能够立刻看到,这就是可见性)
  2. 线程切换带来的原子性问题: 原子性是一个或多个操作在CPU执行的过程中不被中断的特性. 那为什么会中断呢?原因就在于提高性能,就和现在的计算机一样,是分时间片来进行任务切换,同时听歌和敲代码,看似是同时发生,其实不是,知识任务之间切换的非常快,做到了看似同时进行.
    在高级程序中,一个看似简单的操作可能需要多条CPU指令来完成,不如说count += 1;CPU指令至少三个,从内存中拿到count值到寄存器,在寄存器中进行加一操作,将结果写入内存,这个过程中可能会发生任务间的切换,比如说另一个线程在写入内存前有进行了一次++操作,这个时候结果就不是想要的结果了,可能例子不合适,但是这个意思就是这个. 而原子性就是保证高级语言层面保证操作的原子性.
  3. 编译优化的有序性问题: 有序性指的是程序按照代码的先后顺序执行. 看起来没问题,本来就应该这样,其实不然,在JVM的知识中有一个叫重排序,就是编译器为了优化性能,有时会改变程序中语句的先后顺序,大部分情况下编译器调整后的顺序是不会影响程序的最终结果,不过也有特殊情况,如下:
public class Singleton {
   
  static Singleton instance;
  static Singleton getInstance(){
   
    if (instance == null) {
   
      synchronized(Singleton.class) {
   
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

上面是经典的双重检查创建单例对象,在我们的印象中new的操作应该是: 分配内存,在内存上初始化对象,地址赋值. 实际上优化后是: 分配内存,地址赋值,初始化. 优化后的顺序就会出现问题,地址赋值后发生了线程切换,这时候其他线程读取到了对象不为null,但是实际上只有地址,这个时候访问成员变量就会出现空指针异常,这个就是编译优化可能会出现的问题.

Final

final修饰变量是告诉编译器: 这个变量生而不变,可以可劲儿优化.在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。下面的例子,在构造函数里将this赋值给全局变量global.obj,这就是逸出(逸出就是对象还没有构造完成,就被发布出去),线程global.obj读取到x有可能读到0.

// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
    
  x = 3;
  y = 4;
  // 此处就是讲 this 逸出,
  global.obj = this;
}

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

Java内存模型: 解决可见性和有序性问题

可见性的原因是缓存,有序性的原因是编译优化,那解决的最直接的办法就是禁用缓存和编译优化,但是有缓存和编译优化的目的是提高程序性能,禁用了程序的性能如何保证? 合理的方案是按需禁用缓存和编译优化,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法,具体的,这些方法包括volatile,synchronized和final三个关键字,以及六项Happens-Before规则

Happens-Before规则

Happens-before指的是前一个操作的结果对后续操作是可见的,具体如下.

  1. 程序的顺序性规则
    这个规则说的是在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作. 简单理解就是: 程序前面对于某个变量的修改一定是对后续操作可见的.也就是前面的代码x=42对于v=true是可见的.

  2. volatile变量规则
    这条规则指的是对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作,即volatile变量的写操作对于读操作是可见的.

  3. 传递性
    这条规则指的是A Happens-Before C,且B Happens-Before C,那么A Happens-Before C,如下图:在这里插入图片描述
    这样就很明显了,x=42 Happens-Before v=true,写v=true Happens-Before 读v=true,那也就是说x=42 Happens Before 读v=true,这样下来,其他线程就可以看到x=42这个操作了.

  4. 管程中锁的规则
    这个规则是指对一个锁的解锁Happens-Before与后续对这个锁的加锁. 管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现.管程中的锁在Java中是隐式实现的,也就是进入同步块之前,会自动加锁,而在代码块执行完后自动释放锁,加锁以及解锁都是编译器帮我们实现的.

synchronized (this) 
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值