Java并发里面有两个很关键的点:
一是,使用同步来避免多线程间同一时刻访问同样的数据。
二是,线程间的通信和数据的共享。
前面的文章已经讲过了同步的机制。
这篇文章会介绍数据的共享。主要从几个方面:
线程间通信;
原子性;
可见性;
重排序;
线程间通信
线程间的通信,有java内存模型来控制。
每个线程都有自己私有的本地内存,线程间共享的变量放在主内存。如下所示:
线程1 <-> 线程1的本地内存 <-> 主内存
线程2 <-> 线程2的本地内存 <-> 主内存
主要过程:
1, 线程1把本地内存更新的数据刷新到主内存;
2, 线程2到主内存读取线程1的数据,更新到线程2的本地内存中。
原子性
原子性是指一个操作不可分割。
基本类型除long,double之外,读写操作,是原子的,线程安全, 如i=1是原子的;
long和double,JVM分离成了2个32位的操作,所以对long和double的读写,非原子非线程安全;
int i++等操作非原子非线程安全;
volatile修饰的变量的读写,是原子操作,如volatile=1; 但volatile的复合操作,如volatile++,是非原子的;
不可变对象(final)的初始化,原子的和线程安全的。
原子变量类
java提供原子操作的类:java.util.concurrent.atomic包下面。
如:AtomicInteger, 能够原子读-改-写操作,比锁轻量级,性能更高。
可见性
可见性是指一个线程修改了对象的状态之后,另一个线程能够马上看到状态的变化。
可以通过下面的方式保证可见性:
final:初始化final字段确保可见性
volatile:读写volatile字段确保可见性.给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。
synchronized:同步块内读写字段确保可见性
happens before:遵守happen before次序可见性(下一节详细讲解)
重排序
重排序是指为了提高性能,编译器和处理器会对指令进行重排序。
在多线程编程中,我们常常会看到明明写到后面的程序,居然比行数更靠前的程序先执行,这就是程序进行了重排序。
重排序是有一定的规则的:
1,数据依赖性
两个操作访问同一变量,其中一个操作为写操作,那么两个操作间就存在依赖性。
如: a=1; b=a;(写变量a,在读取a)
对于有依赖性的指令,不会进行重排序。
2,as-if-serial语义
不管怎么排序,程序的执行结果都不能改变。
为了遵守as-if_serial语义,对于有依赖性的操作,不会重排序;
如果操作之间不存在依赖,则可能被重排序。
如:
int a=1; //操作1
int b=2; //操作2
int c=a+b; //操作3
操作3一定在1,2之后, 而1可能在2之前,也可能在2之后。
3, happens before
从jdk5开始,java使用happens before来描述可见性。
如果一个操作对另一个操作可见,则两个操作之间必须要存在happens before关系。
其规则总结如下:
1)如果A一定在B之前发生,则A和B存在happens before;
2) 写volatile变量一定发生在后续对它的读之前;
3) Thread.start一定发生在线程中的动作之前;
4) 一个线程调用另一个线程的interrupt一定发生在另一线程发现中断之前;
5) 一个对象的构造函数结束一定发生在对象的finalizer之前;
6) 传递性: A发生在B之前,B发生在C之前,A一定发生在C之前。