Java并发编程系列(一):Java并发内存模型

一、进程间通信方式

线程是借鉴了进程的工作方式,所以我们有必要先看一下进程间通信的方式。
1、管道:这个大家应该比较熟悉,这里主要是父子进程的通信。
2、有名管道:主要是给无亲缘关系的进程传递数据使用,Linux命令中也可以常常使用管道来进行数据的传递。
3、信号量:信号量由迪杰斯特拉提出,用一个整型变量来累积唤醒次数来控制多个进程对资源的访问。
4、消息队列:通过存放进程产生的消息来进行通信,相对于管道来说更加强大
5、信号:用于通知进程事件已经发生。
6、共享内存:共享内存映射了一段多个进程都可以访问的内存,相比于其他方式速度快,当然也更加复杂,常常配合上面的通信方式一起使用。
7、套接字:套接字不仅仅可以用于网络通信,同时还可以提供给本机的进程使用。

二、Java并发编程模型需要解决的问题

对于线程来说,线程之间如何通信以及线程之间如何通信与同步是关键问题。Java中的线程使用了共享内存的的并发模型,线程之间通过读-写内存中的公共变量进行隐式的通信。所以线程间通信问题不用考虑,同步则是Java并发模型需要解决的问题。

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存的并发模型中,同步是显示执行的,程序员必须指定某段代码需要互斥执行。

三、Java内存模型浅谈

由于Java内存模型的内容较多,留到Java虚拟机的博文中,这里简要介绍一下,Java中,类的实例、数组等内容都存储在堆内存中,堆内存是所有现场共享的。而方法区(保留了方法的定义信息)、局部变量和异常参数等都是线程私有的,不会被共享。

Java中,线程之间共享的变量存储在主内存中,每个线程私有的变量存储在线程的本地内存中,本地内存中存储了共享内存中变量的副本,以此来进行读写。

假设有线程A和线程B同时操作一个公共变量X,线程A对X进行赋值,赋值后将本地内存刷新到主内存中,然后线程B读取主内存的值回本地线程。所以线程间的通信必须依靠主内存的交互。Java中多线程的同步主要也是在主内存的基础上做文章。

四、指令重排

现代的CPU和编译器常常会对我们程序中的指令做重新排序。重排分成三种:

  • 编译器优化重排:编译器在不改变单线程语义的前提下重新安排顺序。
  • 指令级并行重排:处理器采用指令级并行技术重叠执行多条指令,会改变执行顺序。
  • 内存系统重排:处理器使用缓存,这也会使得加载和存储乱序。

所以从源代码到实际的指令可能会经过上述三个重排。对于编译器,Java内存模型禁止了一些重排,但是不能保障其他的重排方式。所以重排一旦发生就会出现内存可见性问题。

happens-before

happens-before是JSR-133内存模型中的概念,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作就必须有happens-before关系(下方用h-p简称),和我们密切相关的规则:

  • 程序顺序规则:在单线程中,每个操作和后续操作都是h-p规则,也就是说一个操作的结果对于后续操作都是可见的。
  • 监视器锁规则:对一个锁的解锁和随后对锁的加锁肯定也是符合h-p规则。
  • volatile变量:对于任一个volatile域的写和写后的读符合h-p规则,也就是说多线程操作同一个volatile变量,只要一个线程写完,另一个线程马上就能读到这个volatile域。
  • 传递性:A h-p B,B h-p C,则 A h-p C。

五、volatile的内存语义

在并发编程中,被volatile声明的变量会有特殊的语义实现。

public class VolatileTest{
    private volatile int v =1;

    public void set(int v){
        this. v = v;

    } 

    public void getAndIncrement(){
        v++;
    }

    public int get(){
        return v;
    }
}

上面这段代码中,使用了volatile变量,相当于给get() 和 set()方法上了锁,但是针对v++操作,这个地方无法保证原子性。

在内存的角度,当一个线程对volatile变量写入后,会立即将这个变量刷新到主存中,同时通知下一个读这个volatile变量的线程使线程中的这个变量内存失效,必须从主存中读取,所以这样实现了volatile的功能。

先更新到这里,后面会继续更新。

你可能感兴趣的:(Java,Java并发编程)