深入理解java并发编程基础篇(二)-------线程、进程、Java内存模型

一、前言

  通过前面的学习,我们了解到一些关于并发编程的一些基本概念,这一篇将继续总结以及复习基础篇的内容。

二、进程以及线程

2.1 什么是进程?

  进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

  在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

  如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程:

2.2 什么是线程?

  线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
  那么下面我们会从jvm角度来分析线程与进程的关系。

2.3 图解线程与进程的关系

下面是简略版的图解:


  从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

三、Java内存模型

  JMM(java内存模型),由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序中数据访问一致性和安全性将会受到严重挑战。如何保证一个线程可以看到正确的数据呢?这个问题看起来很白痴。对于串行程序来说,根本就是小菜一碟,如果你读取一个变量,这个变量的值是1,那么你读取到的一定是1,就是这么简单的问题在并行程序中居然变得复杂起来。事实上,如果不加控制地任由线程胡乱并行,即使原本是1的数值,你也可能读到2。因此我们需要在深入了解并行机制的前提下,再定义一种规则,保证多个线程间可以有小弟,正确地协同工作。而JMM也就是为此而生的。

  JMM关键技术点都是围绕着多线程的原子性、可见性、有序性来建立的。我们需要先了解这些概念。

3.1 原子性

  原子性是指操作是不可分的,要么全部一起执行,要么不执行。在java中,其表现在对于共享变量的某些操作,是不可分的,必须连续的完成。比如a++,对于共享变量a的操作,实际上会执行3个步骤:

1.读取变量a的值,假如a=1

2.a的值+1,为2

3.将2值赋值给变量a,此时a的值应该为2

这三个操作中任意一个操作,a的值如果被其他线程篡改了,那么都会出现我们不希望出现的结果。所以必须保证这3个操作是原子性的,在操作a++的过程中,其他线程不会改变a的值,如果在上面的过程中出现其他线程修改了a的值,在满足原子性的原则下,上面的操作应该失败。

java中实现原子操作的方法大致有2种:锁机制、无锁CAS机制,后面的章节中会有介绍。

3.2 可见性

  可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

  首先看一下Java线程内存模型:


  线程需要修改共享资源X,需要先把X从主内存复制一份到线程的工作内存,在自己的工作内存中修改完毕之后,再从工作内存中回写到主内存。如果线程对变量的操作没有刷写回主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

共享变量可见性的实现原理:

1.线程A在自己的工作内存中修改变量之后,需要将变量的值刷新到主内存中
2.线程B要把主内存中变量的值更新到工作内存中

关于线程可见性的控制,可以使用volatile、synchronized、锁来实现,后面章节会有详细介绍。

3.3 有序性

  有序性指的是程序按照代码的先后顺序执行。 为了性能优化,编译器和处理器会进行指令冲排序,有时候会改变程序语句的先后顺序。

比如下面的例子:

	int a = 1;  //1
	int b = 2;  //2
	int c = a + b;  //3
复制代码

经过编译器以及处理器优化后,有可能会变成下面的顺序:

    int b = 2;  //1
	int a = 1;  //2
	int c = a + b;  //3
复制代码

上面这个例子调整了代码执行顺序,但是并不会影响程序执行的最后结果。

那么我们再来看看一个例子(单例是实现--双重加锁实现方式):

package com.MyMineBug.demoRun.test;

public class Singleton {
	static Singleton instance;

	static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null)
					instance = new Singleton();
			}
		}
		return instance;
	}
}
复制代码

未被编译器优化的操作:

指令1:分配一款内存H

指令2:在内存H上初始化Singleton对象

指令3:将H的地址赋值给instance变量

编译器优化后的操作指令:

指令1:分配一块内存W

指令2:将W的地址赋值给instance变量

指令3:在内存W上初始化Singleton对象

如果此刻有多个线程执行这段代码,会出现意想不到的结果。

那么单例模式的创建怎样才是最佳的呢?我们再后续讨论。

  如果觉得还不错,请点个赞!!!

  Share Technology And Love Life

猜你喜欢

转载自juejin.im/post/5d804407e51d45620b21c46c
今日推荐