线程之间如何通信:
通信是指线程之间以何种机制来交换信息
通信机制有两种:共享内存和消息传递
线程之间如何同步:
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序
重排序分3种类型,第一种属于编译器重排,后两种属于处理器重排:
JMM通过禁止特定类型的编译器重排序和处理器重排序,提供内存可见性保证
由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致
JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系(即保证释放锁和获取锁的两个线程之间的内存可见性)
happens-before规则如下:
对于下面代码,假设线程A执行writer()方法,随后线程B执行reader()方法:
class MonitorExample {
int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
……
} // 6
}
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
int a = 10;
int b = 20;
int c = a + b;
执行顺序可以是a->b->c,也可以是b->a->c
假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
Public void reader() {
if (flag) { // 3
int i = a * a; // 4
……
}
}
上面代码中,操作1和操作2没有数据依赖关系,且操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两对操作重排序:
处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照
数据竞争的定义:在一个线程中写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序
顺序一致性:如果程序是正确同步的,程序的执行将具有顺序一致性(即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)
假设有两个线程A和B并发执行。其中A线程有3个操作,它们在程序中的顺序是A1→A2→A3。B线程也有3个操作,它们在程序中的顺序是B1→B2→B3
JMM中没有上述保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致
顺序一致性模型中,所有操作完全按程序的顺序串行执行。在JMM中,临界区内的代码可以重排序(因为JMM目的是在不改变程序执行结果的前提下,尽可能优化编译器和处理器)
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响
把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
为了实现volatile的内存语义,编译器在生成字节码时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
以1.4节中的线程A和B为例,因为2 happens-before 5,所以线程B获取同一个锁之后,共享变量将立刻变得对B线程可见
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了消息(即线程A
对共享变量做出修改)
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的消息(即在释放这个锁之前对共
享变量做出修改)
happens-before的概念来指定两个操作之间的执行顺序(这两个操作可以在一个线程之内,也可以在不同线程之间)
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行(假如重排序后执行结果一致也是可以的)
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变
ThreadB.start()
,则A线程的ThreadB.start()
操作happens-before于线程B中的任意操作ThreadB.join()
并成功返回,则线程B中的任意操作happens-before于线程A从ThreadB.join()
操作成功返回。双重检查锁定1:
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
在某个线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。因为
instance = new Instance();
内部可能会发生重排序
双重检查锁定2:使用volatile禁止A2和A3的重排序
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();
}
}
return instance;
}
}
可以使用类初始化的方案解决双重检查锁定1中的问题:因为初始化实际是执行方法,该方法线程安全
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance ;
}
}