阅读《Java高并发编程详解》后的笔记。
并发编程的三个重要特征
- 原子性
指在一次的操作或者多次操作中,要么所有的操作全部得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
典型代表:银行转账。
tip1:两个原子性的操作结合在一起未必是原子性的,如i++,其中 get i,i+1,set i=x都是原子性操作,但是不代表i++是。
tip2:volatile不保证数据的原子性,synchronized保证。
- 可见性
指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
- 有序性
指程序代码在执行过程中的先后顺序,由于Java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的先后顺序。如:
int x = 10;
int y = 0;
x++;
y = 20;
y=20可能会在x++之前执行,这就是指令重排序。处理器为了提高程序运行效率,可能不会百分百保证代码的执行顺序严格按照编码中的顺序进行,但会保证程序的最终运行结果是编码时所期望的这样。
单线程下 ,无论怎样的重排序最终会保证程序执行的结果和顺序执行的结果一致。多线程下有序性得不到保证,运行结果得不到保证。
JMM(Java内存模型)保证三大特性
- JMM与原子性
Java中对基本数据类型变量的读取和赋值,以及对引用类型变量的读取和赋值都是原子性的,但以下几种情况可能弄错:
(1)x=10;赋值操作 ---------原子性操作
(2)y=x;赋值操作 -----------非原子性操作
1)执行线程从主存获取x到工作内存(或者已在工作内存直接获取)
2)在工作内存中修改y的值为x,然后将y的值写入主存中。
第一和第二步都是原子性操作,合在一起就不是了。
(3)y++;自增操作------------非原子性操作 原因上文三大重要特性之原子性tip1提过。
(4)z=z+1,加一操作(与自增等价)------------非原子性操作
JMM只保证基本读取和赋值操作的原子性操作,其他都不保证。synchronized后者JUC的lock(如使得int类型的自增操作具备原子性,可使用这个包下的java.util.concurrent.atomic.*)
volatile不具备保证原子性的语义。
- JMM与可见性
Java提供以下三种方法保证可见性:
(1)使用volatile
读操作:当一个变量被volatile修饰时,对于共享资源的读操作会直接在主内存中进行,也会缓存到工作内存中,当其他线程对该共享变量进行了修改,会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中烦恼再次获取;
写操作:对共享资源的写操作先修改工作内存,修改结束后立即刷入主存。
(2)使用synchronized
通过synchronized保证可见性。保证同一时刻只有一个线程获得锁,然后执行同步方法,并确保锁释放之前,会将对变量的修改刷新到主存中。
(3)JUC的显式Lock。
volatile具备保证可见性的语义。
- JMM与有序性
Java提供以下三种方法保证有序性:
(1)使用volatile
(2)使用synchronized
(3)JUC的显式锁Lock。
Java内存模型具备一些天生的有序规则,不需要任何同步手段,这个规则被称为happens-before原则。
volatile具备保证有序性的语义。
volatile
用法:只能修饰类变量和实例变量。不能修饰方法参数、局部变量、实例常量,类常量。
语义:保证可见性和顺序性。
public class ValotileFoo {
final static int MAX =5;//init_value最大值
static volatile int init_value = 0;//init_value初始值
public static void main(String args[]){
//启动一个reader线程,发现local_value与init_value不同时,输出init_value 的修改信息。
new Thread(()->{
int localValue = init_value;
while(localValue < MAX){
if(init_value != localValue){
System.out.println("The init_value is updated to" + init_value);
localValue = init_value;
}
}
},"reader").start();
//启动一个update线程,修改init_value的值,localValue>=5的时候退出生命周期
new Thread(()->{
int localValue = init_value;
while(localValue < MAX){
++localValue;
System.out.println("The init_value will be changed to" + localValue);
init_value = localValue;
try{
//短暂休眠,让reader来得及输出变化内容
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
}
},"update").start();
}
}
上面这段代码充分体现了valotile保证共享变量在多线程间的可见性。
具体步骤如下:
1)reader线程从主存中获取init_value = 0,缓存到本地工作内存中;
2)update线程将init_value的值在本地工作内存中修改为1,然后立即刷新到主存;
3)reader线程在本地工作内存中init_value失效(反映到硬件上就是L1或者L2的cache Line失效);
4)由于reader线程在本地工作内存中init_value失效,需要到主存中重新获取init_value的值。
原理和实现机制:
- 被volatile修饰的变量存在于一个“lock”的前缀,相当于一个内存屏障:
- 确保指令重排序时不会讲后面的代码排到内存屏障之前,反之亦然;
- 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成。
- 强制将线程的工作内存中的值刷入主存;
- 如果是写操作,导致其他线程的工作内存(CPU Cache)中的缓存数据失效。
volatile与synchronized
(1)使用
valotile只能修饰类变量和实例变量。不能修饰方法、方法参数、局部变量、实例常量,类常量。
synchronized只能用于修饰方法和语句块,不能用于变量的修饰。
(2)原子性
valotile不保证。
synchronized保证,同步代码无法被中途阻断。
(3)可见性
valotile通过机器指令“lock”使得其他线程工作内存中数据失效,不得不到主内存再次加载。
synchronized借助JVM指令monitor enter 和monitor exit使用同步代码串行化,在monitor exit时所有共享资源刷新到主内存。
(4)有序性
valotile禁止JVM编译器以及处理器对其进行重排序。
synchronized修饰的同步代码块保证顺序性,串行化执行。但在synchronized修饰的代码块中代码也可能发生指令重排序。
如:x,y谁先定义谁先运算可能重排序,但是对程序没有任何影响,因为最终结果x=11,y=21。保证了最终输出结果和代码编写顺序的结果一致。
synchronized(this){
int x = 10;
int y = 20;
x++;
y = y+1;
}
(5)其他
valotile不会使线程陷入阻塞。
synchronized会使线程陷入阻塞。