乐趣区

关于java:Java-SE-多线程内存模型

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

线程间的通信机制, 蕴含两种形式: 共享内存和消息传递

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

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

*

线程间的同步


同步是指程序用于管制不同线程之间 操作产生绝对程序 的机制.

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

消息传递时, 音讯的发送必然在接管之前, 因而同步是隐式的.

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


java 内存模型


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

线程 A 与线程 B 之间如要通信的话, 必须要经验上面 2 个步骤:

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, 其更大的作用是避免指令重排序.

退出移动版