并发的三种特性即 原子性,可见性和有序性;
原子性
即一组操作,全部执行的过程中不能被打断,要么就全部不执行;在我们讨论一个操作是否具有原子性时,其实就讨论这个操作的多个子操作,在实际执行的过程中是否会被随机调度打断;(理解原子性要有个范围的意识,大到我们对一个对象的增删查改,可以通过lock工具来保证原子性,小到具体某一个动作的指令,比如赋值操作可能分为载入数据,计算数据,写入数据,而载入和写入可能还能再分为好几条指令);
所以关于”基本数据类型的访问读写是具有原子性的”这一描述,我觉得应该指的是每次访问读写的时候,都是一次性将64位数据读写完,即一条指令搞定,而不是想long这种64位的数据,在32位操作系统可能需要分两次读写完,即先读写前32位,在读写后32位;
而从这个角度去理解”Writes to and reads of references are always atomic”这句话,就明白,引用仅仅只是存储实际对象的地址,他的访问读写和int是一样的,都是存储的64位或者32位数据,一条指令即可完成操作;
atomicity does not mean “all other threads will be blocked until the value is ready.” It means “all other threads will either see the state purely before the operation is done or purely after the operation is done, but nothing else.” 所有其他线程要么在操作完成前看到状态,要么在操作完成后看到状态;
可见性
可见性是指当一个线程修改了共享变量之后,其他线程能够立即得到这个修改;因为线程修改了共享变量只是先保存在自己的工作内存中,可见性就要求他要立即将该修改同步到主内存,并且其他线程在使用该共享变量的时候要重新从主内存中刷新以获得最新的值
有序性
因为有指令重排的优化,会将代码执行的指令顺序重排,但是最终结果一直,这对依赖被重排代码中的中间值的其他线程来说,可能导致意外的结果,所以为了安全也需要保证指令的顺序执行;
如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义(Within-Thread As-If Serial Semantics)”,后半句是指“指令重排序”和“工作内存和主内存同步延迟”线程。
public class VolatileTest3 {
private static Config config = null;
private static boolean initialized = false;
public static void main(String[] args) {
// 线程1负责初始化配置信息
new Thread(() -> {
config = new Config();
config.name = "config";
initialized = true;
boolean ini2 = initialized;
}).start();
// 线程2检测到配置初始化完成后使用配置信息
new Thread(() -> {
while (!initialized) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
}
// do sth with config
String name = config.name;
}).start();
}
}
class Config {
String name;
}
如何理解“在本线程内观察,所有操作都是有序的”?
我们看上面的代码,这是一段初始化配置信息的代码;我们现在只看线程1中代码,此时initialized只是普通变量,普通变量仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,但不能保证变量赋值操作的顺序与程序代码中的执行顺序一致;而在同一个线程中的方法无法感知到这一点,即JMM中描述的“线程内表现为串行为语义”
如何理解“如果在一个线程中观察另外一个线程,所有的操作都是无序的”?
所谓观察,其实就是本线程依赖到另外一个线程中的值,比如上面的例子中,线程2依赖线程1中的对initialized的赋值。所谓无序,就是说线程2中读取到的initialized值发生改变的顺序,和线程1中修改initialized变量的顺序,不一定一致。因为这里我们线程2希望,前面的config都执行完后,才将initialized赋值为true,线程2才能获取到正确的config值;
离谱的例子;没明白原因
public class VolatileTest4 {
// a不使用volatile修饰
public static long a = 0;
// b使用volatile修饰
public static volatile long b = 0;
// c不使用volatile修饰
public static long c = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (a == 0) {
// long x = b; //注释掉之后,两个线程都陷入了死循环a更新的值都没有刷新到对应的线程内存中
}
System.out.println("a=" + a);
}).start();
new Thread(()->{
while (c == 0) {
// long x = b;
}
System.out.println("c=" + c);
}).start();
Thread.sleep(100);
System.out.println("end sleep");
a = 1;
b = 1;
c = 1;
}
}
synchronized对以上三种的支持
- 原子性:synchronized块之间的操作具备原子性,因为同一时刻只允许一个线程进入synchronized块,保证了其他线程能看到状态时都是操作完成之前或者之后了;
- 可见性:synchronized块的可见性是由『对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store 和 write 操作)』这条规则获得的;
- 有序性:synchronized块是由『一个变量在同一时刻只允许一条线程对其进行 lock 操作。』这条规则获得的,这条规则决定了持有同一个锁( lock 操作的变量)的两个同步块只能串行的进入。