Java并发编程的艺术(三)
线程是如何进行通信的?
- 通过共享内存:通过共享内存的读写操作进行隐式的通信
- 通过消息传递:没有共享内存时,就需要显示的发送消息来进行通信
线程是如何同步的呢?
首先同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
- 通过共享内存:需要通过对共享内存加锁进行线程间互斥来显示地进行线程同步。
- 通过消息传递:因为消息发送一定在消息接收之前,所以是隐式的进行了线程同步。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
jVM的内存结构
线程共享的内存区域:实例、静态变量、数组对象。
- java是通过JMM(内存模型)来控制线程间通信的: 它决定一个线程对共享变量的写入何时对另一个线程可见。因为每个线程都会在本地内存中保存一份共享内存的副本,所以线程间通信是需要线程A先将自己的修改存入到主存中,然后线程B读取该共享内存数据。因此。JMM通过控制线程跟主存之间的交互来为Java程序员提供内存可见性保证。
- 数据依赖性:如果两个操作同时访问一个变量,且这个两个操作中存在写操作,就会产生数据依赖性。
- as-is-serial:不管怎么重排序,单线程(程序)运行的结果是不能改变的。
- 顺序一致性:程序执行的结果与程序在顺序一致性内存模型中执行的结果一致。
从源码到指令序列的重排列
编译器和处理器为了提高程序性能进行优化的一种方式。
- 编译期优化重排序:在程序编译期间,会在不影响单线程语义的情况下,调整语句的执行顺序。
- 指令级并行重排序:现代处理器采用了指令级并行技术IPL来将多条指令重叠执行。如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
上述的1属于编译器重排序,2和3属于处理器重排序。因为重排序可能会导致多线程出现内存可见性的问题:
- 对于编译器重排序:JMM的编译器重排序规则会禁止特定类型的编译器重排序。
- 对于处理器的重排序:JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
Happens-Before:先行发生原则
是判断数据是否存在竞争,线程是否安全的主要依据。
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系(并不是在它之前执行,只是结果对其可见)。它具有四个规则:
- 程序顺序规则:一个线程中的每个操作都happens-before它后面的任意操作
- 监视器锁规则:对一个锁的解锁要happens-before对它加锁之前
- volatile规则:对一个volatile变量的写要happens-before于任意后续对他的读。
- 对象终结原则:一个对象的初始化先行于其finalize()方法
- 传递性
ReentrantLock:可重入锁
- 可重入,因为是手动加锁释放锁,所以要保证加锁和释放锁的次数一样多。
- 可以响应中断(Sychronized不可以响应中断,一个线程获取不到锁就一直堵塞等待)
- 可以尝试获取锁:trylock()
- 可以实现公平锁,但默认是非公平锁。公平锁指在锁上等待时间最长的线程将获得锁的使用权。
锁的实现过程:- 公平锁和非公平锁的所释放都会写一个volatile变量state;
- 公平锁在获取锁的时候会读取volatile的变量;而非公平锁在获取锁的时候会利用CAS更新一次volatile变量state的值;
因此锁的内存语义一般有两种形式:1)利用volatile变量的读写; 2)利用CAS附带的volatile内存读写的语义。
concurrent包的实现
- 首先,声明共享变量为volatile。
- 使用CAS的原子条件更新来实现线程之间的同步。
- 配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
final域的内存语义
- 写final域的重排序规则禁止把final域的写重排序到构造函数之外。
- 初次读一个包含final域的对象的引用,域随后初次读这个final域,这两个操作之间不能重排序
- 在构造函数返回前,被构造对象的引用不能为其他线程所见