Java SE 多线程-内存模型

java内存模型,java memory model(JMM)

线程间的通讯机制,包含两种方式:共享内存和消息传递java

共享内存:线程之间经过共享程序/进程内存中的公共状态,从而进行通讯.例如共享对象.程序员

消息传递:经过明确的发送消息来进行显示的通讯.例如java中的wait()和notify()缓存

*安全

线程间的同步


同步是指程序用于控制不一样线程之间操做发生相对顺序的机制.多线程

共享内存时,同步必须显示进行.即程序员必须指定某段代码在进程间互斥执行.架构

消息传递时,消息的发送必然在接收以前,所以同步是隐式的.并发

Java的并发采用的是共享内存模型ide


java内存模型


从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系;线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本.本地内存是JMM的一个抽象概念,并不真实存在.线程

线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:code

1.首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2.而后,线程B到主内存中去读取线程A以前已更新过的共享变量。

*

JVM对内存模型的实现


JVM在内存中建立两个区域,线程栈区和堆区

每一个线程拥有独立的线程栈(也称为Stack,虚拟机栈,栈),其中存放着栈帧(也成为Stack Frame,方法栈).线程每调用一个方法就对应一个栈帧入栈,方法执行完毕或者异常终止就意味着该栈帧出栈(销毁).栈帧中存放当前方法的局部变量表(用于存放局布变量)和操做数栈(用于算数运算中操做数的入栈和出栈).

栈帧的内存大小在编译时就已经肯定,不随代码执行而改变.可是线程栈能够动态扩展,若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常(即内存溢出);若是虚拟机栈能够动态扩展,若是扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常(即内存溢出)

只有位于栈顶的栈帧才是当前执行栈帧,成为当前栈帧,对应的方法成为当前方法.

线程栈中的局部变量(具体来讲是栈帧中的局部变量),对于其余线程不可见.在传递时也只是传递对应的副本值.若是局部变量时基本类型,那么直接存储其值,若是是引用类型,那么在栈(栈帧)中存储其堆地址,其具体内容存放在堆中.

堆中的对象能够被多线程所共享,只要对应线程有其地址便可.

*

硬件内存架构


CPU从寄存器中读取操做数,寄存器从CPU缓存(可能有多级)中读取数据,CPU缓存从内存/主存中读取数据.

当CPU须要访问内存时,实际过程是:内存将数据传递给CPU缓存,CPU缓存将数据传递给寄存器.当CPU须要写入数据到内存时,也是按照这个逆向流程,可是,CPU缓存只会在特定的时间节点上将数据刷新到内存中.

因此会出现下面代码中的问题:

public static void main(String\[\] args) throws InterruptedException { MyRunnable r = new MyRunnable(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
} 
static class MyRunnable implements Runnable { 
    static int count = 0;
    
    @Override public void run() { 
        while (true) if (count < 100) { 
            System.out.println(Thread.currentThread() 
                    + ":" + count++); 
            } else{     
                System.out.println(Thread.currentThread() 
                    + " break :" + count); 
                break; 
            } 
        } 
}

部分执行结果:

Thread[Thread-3,5,main]:84
    Thread[Thread-10,5,main]:99
    Thread[Thread-5,5,main] break :100
    Thread[Thread-0,5,main] break :100
    Thread[Thread-1,5,main]:98
    Thread[Thread-1,5,main] break :100
    Thread[Thread-4,5,main]:97
    Thread[Thread-4,5,main] break :100
    Thread[Thread-2,5,main]:96
    Thread[Thread-9,5,main]:95
    Thread[Thread-6,5,main]:94
    Thread[Thread-9,5,main] break :100
    Thread[Thread-2,5,main] break :100

能够看到,部分线程已经将count值加到100,后续仍有线程输出的值不足100,缘由在于这部分线程在计算时,cpu是从cpu缓存中读取的count备份,并且此备份并不是最新值.

执行时刻靠后的线程读取到"旧值"的线程称为脏读.

使用volatile关键字能够保证没有脏读,即保证变量的可见性.每一个线程修改的结果能够第一时间被其余线程看到.由于cpu再也不从cpu缓存中读取数据而是直接从主存中读取.

可是仍然会存在线程不安全的问题,缘由在volatile仅保证数据可见性,可是不保证操做原子性(即一个操做或者多个操做要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行).

上述程序的第25行,实际执行过程当中须要读取count值,控制台输出,count值加1,返回.volatile仅保证后执行的线程读取到的值不会比先执行的线程读的值更"旧".可是可能存在一种状况:线程A读取count值(假如是50)后,时间片丢失,线程B拿到时间片,读取count值(也为50)并完整执行该方法(count值为51),而后线程A恢复执行,因为已经读取过count值因此再也不执行,执行结果与线程B相同(也为51),从而仍有线程不安全.

为了保证线程安全,仍须要使用锁或者同步代码块,由于在解锁以前,必然将相应的变量值刷新到内存中.

至于volatile,其更大的做用是防止指令重排序.

相关文章
相关标签/搜索