并发编程的基础知识

  • 关于进程与线程

        为什么会出现进程?

       现在的计算机可以用来做很多的事情,而早期的计算机原本是用来做数学运算的。用户输入什么指令,计算机就做什么操作,当用户在思考或者输入数据的过程中,计算机可能什么都不会做,这样的效率非常低。

       能不能把指令先写好,存在计算机中,让计算机自己去读取执行呢?于是批处理操作系统就诞生了。人们先把程序的指令写好交给计算机自行读取执行。但是新的问题又诞生了。当一个任务在进行大量的IO操作而CPU空闲时,其他任务此时并不能利用到CPU的资源,必须等待该任务执行完才能去使用CPU,这样就导致资源大量浪费。

      那能不能等到A任务执行一半(比如IO的时候),让B任务去使用CPU呢?实现这样的效果几个问题必须解决。

      内存中同时有两个程序的运行数据,那么如何分辨?任务的切换又如何实现,如何返回到切换之前的状态呢?

      于是聪明的人类就发明了进程的概念,不同进程有其独立的内存进程空间,运行时候互不干扰。进程的控制块保存了进程的运行状态,使得进程之间的切换成为可能。

     这就是并发,CPU的使用在多个任务之间来回切换,使人看起来好似在同一时间可以操作多个任务的感觉。

     为什么会出现线程呢?

     由于一个进程在同一时间内只能做一件事情,例如一个音乐软件进程,在播放音乐的同时,还应该支持搜索啊等功能。在这种情况下播放音乐的同时,我们的搜索只能等待了。引入了线程之后,线程就相当于进程中的子任务,这些子任务是可以在进程的层面上进行并发执行的。

  综上:进程是在操作系统层面实现的并发,线程是在进程的层面上实现的并发。一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程是操作系统分配资源的基本单位,而线程是操作系统实现调度的基本单位。

  • 关于线程安全

      维基百科中关于线程安全的定义:线程安全是编程中的术语,指某个函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

       什么是共享变量呢?也就是多个线程而已操作的变量,我们知道进程中的多个线程是共享进程中的数据的。在JVM中,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。

      在Java中一共有三种变量:类变量,成员变量,局部变量。其中类变量在类加载就初始化,是在方法区中的,成员变量属于对象,而对象是分配在堆内存中的,局部变量是方法内部的变量,是在栈中的。所以类变量与成员变量是线程共享的。

      线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个共享数据时,进行保护,其他线程不能进行访问直到该线程  读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

  • 计算机硬件带来的问题

      原子性,一致性,有序性 这三个是计算机硬件在并发编程领域需要注意的问题。

      原子性:这个问题是由CPU时间片的原因引起的。cpu会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。比如i++,这个操作并不是原子性的。它分为三个步骤,读取i, 执行操作,写入内存。共享变量就会被多个线程同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。

     一致性:这个问题是cpu的缓存引起的,由于cpu与内存之间的速度差异,人们引入了高速缓存。

单线程。 cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

单核CPU,多线程。 进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

多核CPU,多线程。 每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

有序性:由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。

Java中如何解决计算机硬件带来的三个问题?

        导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。合理的方案应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则

  1.关于volatile:一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰后,那么就具备了两层语义

       1)保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是 立即可见 的;

  2)禁止进行指令重排序。

   volatile可以保证原子性吗?不能。

2.关于synchronized:

   

    

 

      

发布了29 篇原创文章 · 获赞 34 · 访问量 8156

猜你喜欢

转载自blog.csdn.net/weixin_39634532/article/details/90730228