【并发编程】一:java中的并发基础概念

前言

今天聊聊java中的并发的理论知识。这是后面我们分析JUC源码的理论基础。

happens-before原则

原子性,有序性,可见性是并发问题的源头。
CPU 增加了缓存,以均衡与内存的速度差异;却导致了程序的可见性问题!
操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;却导致了原子性问题
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用;却导致了有序性问题
所以总结起来就是 cpu缓存导致了可见性问题,多线程的切换导致了原子性问题,编译优化导致了有序性问题!
那么我们只需要禁用缓存 禁用编译优化就能解决其中的 有序性和可见性问题。但是也不能一股脑的全部都禁用了而是需要合理的禁用。
这个时候JMM就出来了,它是一个很复杂的概念,我们简单的理解一下就是JVM 提供如何按需禁用缓存和编译优化的方法,这些方法包括volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
    这个很容易理解 在一个线程中的前面的操作肯定是对后续操作可见的。

     int a = 1; //1
     int b = 2; //2
     int c = a + b ; //3
    
     3处访问 a,b是一定能访问到的,2处如果要访问 a也是能成功访问的。
    
  • 管程规则:对一个锁的解锁,happens-before于随后对这个锁的加锁

    private int a = 1;
    private synchronized void add(){
     		a = 2;
    }
    private synchronized  void get(){
    	System.out.println(a);
    }
    
     假如线程A执行 add方法 线程B 随即get方法 是一定能获取线程A更改之后的值的。
    
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

  private int a = 1;
 private volatile  boolean flag = false;
 private  void change(){
   a = 3;               //1
   flag = true;      //2
 }
 private void get(){
  if (flag){  //3
    //a = 3  //4
  }
 }
  线程A执行change 线程B随即执行get  获取的a = 3
  • 传递性:如果A happens-before B , B happens-before C 那么A happens-before C

     上面的例子对这个规则也适应:1 happens-before 2  ,2happens-before3 ,1happens-before3
    
  • start()规则:如果线程A执行了B.start()(启动线程B) ,那么A线程的B.start() 操作 happens-before 于线程B中的任意操作

    a = 3;     //1
  Thread thread = new Thread(() -> { 
    System.out.println(a);   //2
  });
  a = 5;   //3
  thread.start(); //3
 代码3 happens-before 2 所以输出a值为5	
  • join()规则:如果线程A执行B.join() 并成功返回,那么线程B中的任意操作happens-before 于线程A从B.join() 操作成功返回
	Thread thread = new Thread(() -> {
      a = 7;  //1
    });
    thread.start(); //2
    thread.join(); //3
    System.out.println(a); //4
1 happens-before 4 这里输出 7

通过上面简单的介绍 synchronized是保证可见性就是 happens-before中的管程规则。
那么JUC包中的Lock是如何保证可见性的呢?

MESA管程模型

并发编程两个棘手的问题:互斥 和同步
互斥:解决同一时刻对共享资源只有一个线程能访问。
同步:解决线程的相互协调通信问题。
上面两个问题是整个行业面临的问题 并不是哪一门语言所面临的问题。所以既然是行业存在的问题那么一定会有行业解决方案-信号量和管程模型

java解决并发问题采用的是实现MESA管程模型。可能你会问:什么是管程?其实就是一个编程模型。只要简单的知道它是为了解决并发问题的编程模型。目前有三种管程模型:Hasen模型、Hore模型、和MESA模型,java语言采用的是MESA管程模型。
我们先看一个图
在这里插入图片描述
上面就是管程模型 其实说白了就是对共享变量的封装 然后提供一个入口,同一时刻只能有一个线程进入管程。其他的线程将会在入口的等待队列中排队。那么我们说并发编程的两个问题其中的互斥是不是就解决了?
同一时刻只有一个线程能进入管程,其他的线程在入口的等待队列中等待!
那第二个问题同步怎么解决呢?
例如线程A进入管程 线程B在入口的等待队列中等待,如果某一个条件满足 线程A调用了一个对象的wait()方法,那么线程A就会在这个对象上等待,更官方一点我们就把这个对象的等待队列称作 条件变量等待队列。那么线程B进来了 在这个对象上调用了notify方法通知线程A可以从这个条件变量队列中醒来了。这样就解决了线程的相互协作问题了。
所以管程模型能解决并发中的两大难题。
上面我们说了线程B调用对象的notify方法通知线程A可以执行了,那么线程A什么时候醒?这个问题就是上述三种管程模型的最主要的区别:

  1. 在Hasen模型中 要求notify方法放到代码的最后,这样B通知完A后 B就结束了,然后A再执行这样就能保证同一时刻只有一个线程执行
  2. 在Hore模型中,B通知完A后,B阻塞(进入阻塞队列),A马上执行;等A执行完之后,再唤醒B。比起Hasen模型B多了一次阻塞唤醒的操作。
  3. 在MESA管程模型中,B通知完A后,B还是会继续执行,A并不立即执行,仅仅是从条件变量的等待队列进入到入口的等待队列(这里注意 A再次执行时 可能条件又不满足了,所以需要循环的方式检查条件变量)。但这样的好处是 notify不用刻意的放在代码的最后,B也没有多余的阻塞唤醒操作。

说到这里 我们java的 synchronized 不正是这样操作的吗!所以 synchronized 就是一个简单的管程模型。为什么是一个简单的管程模型?因为它只有一个条件变量 也就是只有一个条件等待队列。 例如如下代码,当前线程就在 obj上面等待了。

synchronized(obj){
 obj.wait();
}

那JUC中的lock-condition是不是呢?答案是肯定的! 它是一个标准的管程模型的实现,只不过是通过java代码实现的。

通过上面的一通介绍 其实也不难发现 管程就是用几个队列来解决并发编程的互斥和同步问题的。入口等待队列解决互斥问题!条件等待队列解决同步问题! 这个我们后面分析JUC Lock的实现的基础 AQS的时候会再次体现!

结束语

本篇文章介绍比较重要的两个概念:一个是java内存模型中的 happens-before原则,另一个是解决并发编程问题的管程模型。理解了这两个概念对于你去理解synchronized关键字 和JUC包下面的锁会很有帮助。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值