ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

多线程理论基础(一)

2021-02-05 16:00:40  阅读:178  来源: 互联网

标签:缓存 理论 基础 可见 线程 内存 有序性 操作 多线程


前言

文章目录


不管怎么说.并发在任何一门语言里面均处于高级语言的部分,主要是并发部分涉及的内容比较多,常常做业务开发的工程中对于并发使用场景比较少,人一旦是处于一个绝对的舒适区很难做出逆行尝试,所以对于这一块内容也确实需要梳理,学习一下,使得自己知其然,知其所以然; ![在这里插入图片描述](https://www.icode9.com/i/ll/?i=20210205154524719.png?,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYxMTU2Nw==,size_16,color_FFFFFF,t_70#pic_center)

主要按照下面的顺序:

  • 多线程的理解
  • 并发问题的根源:可见性,原子性,有序性
  • JAVA如何解决并发的根源问题

多线程的理解

三个概念:CPU,内存.I/0设备,随着不断的发展,三者之间明显的差距在于三者速度上的差异,CPU的操作速度远远大于内存,更大于I/0设备的读取,这也是很多业务操作,我们所说的内存操作的速度要远远大于读库操作,由于这种硬件上带来的差距,导致各计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:

1.CPU增加缓存,以减少与内存之间的速度差异(体系结构)
2.操作系统增加进程,线程,来分时复用CPU,用来弥补与I/O设备之间的速度差距(操作系统)
3.编译程序优化操作指令.使得缓存更加高效利用

由于这样的优化导致出现了多个线程操作的时候,出现了可见性,原子性,有序性的问题

可见性,原子性和有序性

1.可见性

在这里插入图片描述
单核时代(左):多个线程.操作同一个共享变量,这样修改另外一个线程可见,多核时代:多个CPU有自己缓存,无法保证可见性;

public class ThreadDemoTest01 {
    private long count = 0;

    private void add10K() {
        int idx = 0;
        while (idx++ < 10000) {
            count += 1;
        }
    }
    long get() {
        return count;
    }
    /**
     * 开两个线程:执行add10k带代码
     * 实际输出:10000+10000 = 20000
     *
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        ThreadDemoTest01 test = new ThreadDemoTest01();
        // 创建两个线程,执行 add() 操作
        Thread th1 = new Thread(() -> {
            test.add10K();
        });
        Thread th2 = new Thread(() -> {
            test.add10K();
        });
        // 启动两个线程
        th1.start();
        th2.start();
        // 等待两个线程执行结束
        th1.join();
        th2.join();
        System.out.println(test.count);
    }
}

共享变量count,两个线程执行,会把count这个值写入到各自的cpu缓存中,但是执行的过程中各自缓存对于彼此不可见,导致无法的保证可见性的操作;

2.原子性

单CPU下也可以进行多个操作,你可以在单核的机器上,一边看网页一边听歌,只是由于cpu在多个进程间切换的时间是无法察觉,cpu在操作过程切换的时间称为时间片,在JAVA程序里.这种切换就会造成并发bug的出现,

count += 1

JAVA中一般会认为这种操作假象认为是原子操作,其实不然:

指令1:count从内存加载到cpu寄存器
指令2:寄存器执行+1操作
指令3:写回内存

在这里插入图片描述

引用一下,极客时间王老师的并发课程里的图,之后可能还会引用,导致线程修改丢失,导致并发所说的原子性问题;

3.有序性

java虚拟机会对编译之后的代码进行优化,对指令进行的先后执行顺序进行依赖,这种优化会带来诡异的并发程序bug,最为常见的例子:双重检查创建单例对象的例子:特别注意

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

在这里插入图片描述

一般认为:
1.分配一块内存 M;
2.在内存 M 上初始化 Singleton 对象;
3.然后 M 的地址赋值给 instance 变量。

实际优化执行:
1.分配一块内存 M;
2.将 M 的地址赋值给 instance 变量;
3.最后在内存 M 上初始化 Singleton 对象。

主要的问题在于先给变量赋值初始化的地址.后初始化对象,这样会存在出现子访问instance对象出现NULL的情况;

可见性和有序性解决思路

1.禁用缓存

JAVA内存模型其实是规范了JVM如何提供按需禁用缓存和编译优化,简单的就是要理解三个关键字:

  • volatile
  • synchronize
  • final
  • Happens-Before原则

volatile表示当前修饰的变量只能从内存中读取写入,不能从CPU缓存中获取.示例代码:

class Test {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里 x 会是多少呢?
    }
  }
}

**特别提醒:**多个的线程A调用write方法,v=true写入到内存,线程B执行reader方法,此时变量x的值是0还是42,jdk1.5之前有可能是0或者42,jdk1.5之后就是42,这里是Happens-Before原则;

2.Happens-Before原则

1.程序的顺序性规则
2.volatile 变量规则:写 -> happens-before-> 读
3.传递性:A ->happens-before ->B, B-> happens-before ->C  那么A->happens-before->c

在这里插入图片描述

4.管程中锁的原则:对一个锁的解锁 happens-before 后续对这个的锁的加锁
5.线程start()规则,主线A启动自子线程B,那么主线程之前的start()->happens-before->子线程B操作;
6.线程join()原则:主线线程调用子线程jion()方法,那么子线程执行完,主线程可以知道子线程的所有操作;

3.final关键字

final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化

参考:https://www.pdai.tech/md/java/thread/java-thread-x-key-final.html

原子性解决思路-互斥锁

synchronized关键字就是一种加锁策略,可以用来修饰方法,也可以用来修饰同步代码块.

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object();
  void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
} 

synchronized加锁和解锁操作是隐式的,但是对于synchronized加锁操作有一个隐式规则:

  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
  • 当修饰非静态方法的时候,锁定的是当前实例对象 this。

**特别注意:**class锁和this锁,代表锁限制颗粒度是不同的.

synchronized加锁操作,保证了多个线程操作的情况下,只能保证一个线程进入临界资源区,如何理解保证可见性呢?Happens-Before原则中有一条:

  • 管程中锁的原则:对一个锁的解锁操作Happens-Before于后续对这个锁的加锁

由于这边规则的限制,当多个线程操作临界区的资源时,一个线程加锁操作临界区资源,解锁后由于传递性的原则,对于后续线程操作临界区资源是可见的.

总结

线程主要是从的程序角度提高硬件系统利用效率,由于硬件系统发展的存在给多线程在使用上存在了三个问题的根源:

  • 可见性
  • 原子性
  • 有序性

针对并发问题的根源,思考对于可见性和有序性,在JMM内存结构上和Happens-Before原则上规避可见性和有序性的问题,主要针对下面几个关键词做思考:

  • final
  • Happens-Before原则
  • volatile

互斥锁主要解决原子性的问题,后续主要针对原子性进行分析

关于我

Hello,我是球小爷,热爱生活,求学七年,工作三载,而今已快入而立之年,如果您觉得对您有帮助那就一切都有价值,赠人玫瑰,手有余香❤️.最后把我最真挚的祝福送给您及其家人,愿众生一生喜悦,一世安康!

标签:缓存,理论,基础,可见,线程,内存,有序性,操作,多线程
来源: https://blog.csdn.net/weixin_44611567/article/details/113697617

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有