java多线程基础

摘要:

本文旨在介绍java多线程中的一些基础概念,同时也是自己的多线程学习总结。本文将围绕以下几点展开讲解:线程的定义、java内存模型、并发及同步、java的happens-before原则,最后介绍java中的synchronized以及volatile关键字。

线程及其定义

  • 线程的基本定义

线程是操作系统引入的概念,旨在为了操作系统更充分的利用计算机资源。进程是程序的一次动态执行过程,是操作系统为程序分配资源的基本单位,可以简单的任务,进程间是相互隔离独立的,但是这种粗粒度的划分往往有时候不能精准的控制程序,各进程的相互独立也不利于进程间的相互通讯,因此在进程的基础上又进一步引入了线程这一概念。可以这样认为,进程是由一个个的线程组合而成的。这些线程可以共享其所在进程的公共资源,这样就方便了线程间的相互通信,同时,线程的切换所带来的资源消耗是要远小于进程的。最重要的是,线程才是在cpu真正执行的基本单位。cpu采用时间片轮转的方式进行线程具体执行和切换的基本方式。

  • 为什么要引入多线程编程

引入线程的最主要目的就是为了充分利用cpu资源,现代计算机往往是多核的,所以我们可以让多线程在多个cpu上并发的执行,因为是多个cpu,各线程间可以认为是并行的(也就是同时执行),这样我们就可以充分利用多核计算机的优势,达到计算任务时间的缩短。需要注意的是,在单核计算机中,实际上是没有并行这个概念的,不过线程任务设置为多少,cpu内部一定是将这些线程轮流调度的,在某一时刻,最多只有一个线程占用着cpu。

java内存模型

  • java内存模型剖析

我们需要注意的是,cpu的运算速度是远远与内存的交互速度的,我们都知道简单的cpu模型由以下几部分构成:运算器,控制器,以及为了协调运算器和与内存交互带来的巨大速度差异而引入的cache层。宏观上看,java内存模型如下图所示:

这样的规定只是java内存的抽象模型,我们来看下图实际上java工作内存示意:

同时,我们需要注意的,当线程在操作共享变量时,是分为如下几个步骤的:

  1. 将共享变量从主内存复制到自己的工作内
  2. 对工作内存中的变量进行操作
  3. 将工作内存的变量更新到主内存

我们可以看到,线程对主内存的共享变量操作并不是原子性的,这也就引入了我们下面要分析的共享变量内存不可见问题。

  • 内存模型与多线程模型的冲突

我们都知道cache的思想是由于空间一致性而引入的,因为实际上一段程序的执行,往往在一段时间内频繁的访问内存中的某个区域,如果我们每次都是从内存中加载该区域内的值,那么与cpu的这种高速性势必造成冲突,因此我们引入cache思想,将频繁访问的内存块载入cache中,cache一般由更高速的寄存器组成,因此和cpu的交互速度非常快。但是这样的结构也造成了多线程中的内存不可见问题。下面我们以一个具体的案例来说明:

假设线程A和B此时的两级缓存皆为空,且初始共享变量X在主内存我们依次执行如下操作:

  1. 线程A将内存中共享变量X修改为1,因为此时线程A的两级缓存皆为空,那么此时线程A会将X依次载如缓存,并且将X的值设置为1,然后再将结果写回主内存,此时主内存中X的值为1。
  2. 线程B修改X的值为2,此时一级缓存为命中,但是二级缓存命中,将X的值载入线程B的以及缓存,然后进行修改,再依次修改缓存,然后将结果写回主内存。
  3. 线程A在此访问X的值,并想将其修改,我们会发现这里出现了问题,由于一级缓存就命中了,我们不会再去主内存中查看X的值,而显然先前线程B已经将主内存中的X修改为2了,但是线程A依然用的是过期数据,也就是说,线程B对主内存中所做的修改对线程A是不可见的,这就是共享变量内存不可见。

值的说明的是,在java虚拟机中,将上述cache称为线程的工作内存,并且规定了,任何对于数据的读写操作不允许直接在主内存上进行,因此我们会发现,内存模型中的工作内存,共享变量,多线程三者在一起始终会产生矛盾。下面要讲的多线程并发和同步就是为了解决这样的矛盾。

多线程并发和同步

通常情况下,多核cpu下,多线程的并发就是指多个线程(任务)同时在多个cpu核上执行,通过上述分析我们知道,这种情况下,如果不通过某种机制合理的进行执行,那么势必会造成多个线程对共享变量进行了不正确的读写。我们将多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用的这种机制称为同步。而我们最常用的同步手段应该就是互斥了。而临界区,互斥量,信号量只不过是实现互斥的一种的手段。

既然有互斥实现的同步,那么必然有其他的方式实现的同步,非阻塞式同步就是一种比较高效的同步策略,实际上在程序的执行过程中,并不总是会出现对共享变量的冲突访问的,那么我们就可以采用如下策略,先执行对共享变量的修改,如果真的发生了冲突,那么再后续其他的补偿。

总结如下:同步是为了多线程并发而引入的,其目的是为了多个线程同时对共享数据的操作而带来的不一致问题。同步手段有如下两种,互斥同步和非阻塞同步。互斥的基本思路就是同一时刻只允许一个线程对共享数据进行操作,其他线程会被阻塞,实现互斥的手段主要有临界区,互斥量和信号量。而非阻塞式同步一般不会将其他线程挂起,从而提高并发的速度。

java提供的并发机制

上面分析了这么多,我们正式进入java在多线程编程中为我们提供的便利。

  • volatile

首先介绍volatile关键字,实际上该关键字和上面提到的java内存模型有着密不可分的关系,他的作用就是为了解决java内存模型中共享变量的内存可见性问题。该关键字保证了当变量被声明为volatile时,当线程对数据进行了修改时,不会将共享变量写到其他的缓存区,而是会被立刻刷新到主内存。当其他线程读取该共享变量时,不会使用工作内存中的值,而是从主内存中获取。这样就保证了,即使多个线程对数据进行读写,该变量的值也不会发生错误。

  • synchronized

synchronized关键字提供了上面提高的互斥同步的一种抽象,需要注意的synchronized可以修饰方法或者代码块,当修饰方法时,实际上锁住的当前对象,因为每个对象实际上都有个一个同步锁。可以这样理解,锁是实现临界区互斥访问的一种手段,而synchronized是实现锁的一种抽象,他不需要我们手动的进行加锁解锁,而是用虚拟机为我们实现的。

synchronized关键字原理如下:当进入代码块前,自动的获取内部锁,这时候其他线程再想访问该临界区会被阻塞,拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者调用了内部锁的wait() 系列方法时释放该内置锁,当该线程的锁被释放后,其他被阻塞的线程便会按照调度算法进行临界区的访问。

posted @ 2020-06-18 18:43  smalllll  阅读(149)  评论(0编辑  收藏  举报