专注于快乐的事情

java并发机制和内存模型总结

术语

临界区

临界区表示一种公共资源或者共享数据。每一刻只能被一个线程使用,如果临界区资源被占用,其他线程想使用这个资源,就必须等待。

CAS

Compare and Swap,比较并设置。

用于在硬件层面上提供原子性操作。在Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

并发编程的挑战

并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。

线程有创建和上下文切换的开销、死锁的问题,以及受限于硬件和软件的资源限制。

amdah1定律:多核cpu优化的效果取决于cpu数量和系统中串行化程序的比重。只提供cpu数量也无法提高系统性能。

#java中并发编程模型

Java中所使用的并发机制依赖于JVM的实现和CPU的指令。

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步

通信是指线程之间以何种机制来交换信息。线程之间的通信机制有两种:共享内存消息传递

Java的并发采用的是共享内存模型。由Java内存模型(本文简称为JMM)控制。

在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信

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

在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

操作系统层面:为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作。

现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。

虽然写缓冲区有很多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

如何解决其他处理器缓存的值还是旧的?

为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了。

操作系统层面:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
指令重排,同时带来了乱序的问题。在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

JMM总结

JMM

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的Local内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

共享变量:在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。这些变量统称为共享变量。

JMM关键技术点

JMM遵循的基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都可以。

JMM的关键技术点主要为多线程的原子性/可见性/有序性。

原子性

处理器如何实现原子操作

32位IA-32处理器使用基于对缓存加锁总线加锁的方式来实现多处理器之间的原子操作

Java如何实现原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作。

从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)

使用循环CAS实现原子操作

JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。
自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

可见性

JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

有序性

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。指令重排,同时带来了乱序的问题。

JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

那些指令不能进行重排? happen-before规则。

#常用同步方式

synchronized、volatile、Lock

volatile

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile可以认为是轻量级的synchronized。它在多处理器开发中保证了共享变量的“可见性”。

Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。

volatile的定义与实现原理

Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
例如在每个volatile写操作的前面插入一个StoreStore屏障。

为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

##Lock对象

volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。

内存内存语义

线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
·线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();

public void writer() {
lock.lock(); //
try {
a++;
} finally {
lock.unlock(); //
}
}

public void reader() {
lock.lock(); //
try {
int i = a;
//
} finally {
lock.unlock(); //
}
}
}

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。内部使用整型的volatile变量(命名为state)来维护同步状态。

整型的volatile变量(命名为state)来维护同步状态。
加锁方法首先读volatile变量state。释放锁的最后写volatile变量state。

concurrent包通用化的实现模式
首先,声明共享变量为volatile。
然后,使用CAS的原子条件更新来实现线程之间的同步。
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

synchronized

VS lock

synchronized是托管给JVM执行的,而lock是Java写的控制锁的代码。

synchronized称为“重量级锁”,在1.5中,synchronize是性能低效的。导致有可能加锁消耗的系统时间比加锁以外的操作还多。
到了Java1.6发生了变化。synchronize在语义上很清晰,可以进行很多优化(为了减少上下文切换),有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁,而在CPU转换线程阻塞时会引起线程上下文切换。

Lock用的是乐观锁方式。每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。获得锁的方法是compareAndSetState,就是调用的CPU提供的特殊指令。

Synchonized在JVM里的实现原理

synchronized实现同步的基础:Java中的每一个对象都可以作为

代码块同步是使用monitorenter和monitorexit指令实现
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。

任何对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。

模型介绍

happen-before规则

顺序一致性内存模型

参考

评论系统未开启,无法评论!