Java并发底层原理
Java内存模型
Java内存交互模型是一组内存操作规范,需要JVM实现来遵守,以便开发者可利用这些规范,更方便的开发多线程程序;该规范不依赖处理器,即时使用不同的处理器结果也是一样的,解决了不同处理器对相同代码解释却不同的问题,从而保证并发安全
若没有该规范,可能在不同的JVM上运行的结果不一样,volatile、synchronized、Lock等都将失效,需要我们手动指定内存栅栏(工作内存和主内存之间的拷贝和同步)
重排序
线程内部两行代码时,执行顺序和源代码中的书写顺序不一致,就是发生了重排序,重排序现象是由以下结果导致:
- 编译器优化:包括JVM,JIT实时编译器等
- CPU优化:编译器未发生重排序,CPU也可能对指令进行重排序
- 内存重排现象:由于线程之间的变量不可见性,导致表面上的重排序(其实没有发生重排序)比如:线程1进行修改值,但是未写入主内存时,也就是线程2未看到修改,读出来的是未经修改的值
重排序优化的目的是为了直接允许当前能立即执行的后续指令,避开回去下一条指令所需数据造成的等待,从而提高运行效率
下面代码验证重排序
public class Main {
private static int a = 0;
private static int b = 0;
private static int x = 0;
private static int y = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
a = b = x = y = 0;
Thread thread1 = new Thread(() -> {
a = 1;
x = b;
});
Thread thread2 = new Thread(() -> {
b = 2;
y = a;
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//保证线程1和线程2先执行结束
/**
* thread1先运行完毕,thread2再运行, a=1,b=2,x=0,y=1
* thread2先运行完毕,thread1再运行, a=1,b=2,x=2,y=0
* thread1和2交叉运行,thread1部分运行,thread2完整运行,a=1,b=2,x=2,y=1
* thread1和2交叉运行,thread1部分运行,thread2部分运行,a=1,b=2,x=2,y=1
* thread1和2交叉运行,thread2部分运行,thread1完整运行,a=1,b=2,x=2,y=1
* thread1和2交叉运行,thread2部分运行,thread1部分运行,a=1,b=2,x=2,y=1
* 出现重新排序,可能会出现 a=1,b=2,x=0,y=0
*/
System.out.println("a=" + a + ",b=" + b + ",x=" + x + ",y=" + y);
if (x == 0 && y == 0) {
System.out.println("证实了有重排序");
break;
}
}
}
}
可见性
高速缓存容量比主存内存小,但是速度快,所以为了提高执行效率,CPU与主存之间就多了缓存层(不同的CPU有不同的缓存层数),由于CPU有多层缓存,导致读到数据可能会过期,线程之间的共享变量的可见性问题就是由于缓存引起的,Java内存模型标准就屏蔽了底层的多层缓存,只抽象成了两层:主内存和工作内存
Java中所有的共享的变量都存储在主内存中,每个线程都有自己的工作内存,工作内存中保存该线程使用到的变量的主内存副本拷贝;线程对变量的读写操作都应该在工作内存中完成;不同线程之间不能相互访问工作内存,交互数据需要通过主内存
下面代码验证可见性
public class Main {
private static int a = 1;
private static int b = 2;
public static void main(String[] args) {
while (true) {
a = 1;
b = 2;
new Thread(() -> {
try {
Thread.sleep(100); //休眠更容易出现该效果
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 3;
b = a;
}).start();
/**
* thread1先运行完毕,thread2再运行, a=3,b=3
* thread2先运行完毕,thread1再运行, a=1,b=2
* thread1和2交叉运行, a=3,b=2
* thread1先运行完毕,但thread2只看到了对b的修改却未看到对a的修改 a=1,b=3
*/
new Thread(() -> {
try {
Thread.sleep(100); //休眠更容易出现该效果
} catch (InterruptedException e) {
e.printStackTrace();
}
if (a == 1 && b == 3) { //条件是a=1,b=3
System.out.println("a=" + a + ",b=" + b); //输出结果却可能是其他值
}
}).start();
}
}
}
Happens-Before
由于操作的有执行顺序,由于操作可以在一个线程之内,也可以是在不同线程之间,所以Java内存模型通过happens-before规则解决跨线程的内存可见性问题,简单的说就是:前一个操作的结果可以被后续操作获取
volatile、synchronized、lock、并非容器、join等待、线程启动等都可以保证可见性,具体Happens-Before规则如下:
- 单线程原则:又由于传递性,一个线程内,后面语句一定能看到前面语句
- 锁操作:加锁之后一定能看到解锁之前的指令,以及解锁之前的其他操作
- volatile变量:使用volatile修饰的变量,所有线程都能立即看到该变量的修改,以及之前的其他操作
- 线程启动:父线程启动子线程,子线程一定能看到启动前父线程的所有指令
- 线程join:线程join等待结束后,一定能看到被等待线程的所有指令
- 中断:线程被其他线程中断,中断检测或中断异常一定能被其他线程看到
- 构造方法:
finalize()方法一定能看到构造方法中的最后一条指令 - 工具类的Happens-Before原则
- 线程安全容器get一定能看到在此之前的put等存入动作
- CountDownLatch、CyclicBarrier、Semaphore
- Future
- 线程池
volatile关键字
volatile的作用
volatile只能作用于属性字段上
volatile是一种轻量级的同步机制,因为并不会发生上下文切换等行为开销(无锁机制),相应的能力也小,无法做到synchronized那样的原子保护,只有在很有限的场景下才能发挥作用
- 可见性:读取一个volatile变量之前,需要先适相应的本地缓存失效,这样就一定能从主内存中读取到最新的值;修改一个volatile变量会立即刷新到主内存中
- 禁止指令重排序优化:比如解决单例双重锁乱序问题
不适用的场合
非原子性的数据争用
public class Main {
private volatile static int a;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) a++;
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//保证线程1和线程2先执行结束
System.out.println("最终的a=" + a); //结果不一定是20000
}
}
适用场合
纯赋值的原子操作,由于Happens-Before规则,volatile可作为刷新之前变量的触发器
public class Main {
private volatile static boolean flag;
private static String options;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("读取配置文件");
options = "已赋值配置";
System.out.println("读取配置成功");
flag = true; //由于Happens-Before规则,另一个线程也一定会看到options的修改,从而保证线程安全
});
Thread thread2 = new Thread(() -> {
while (!flag) { //未配置好就陷入休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("配置项为:" + options);
});
thread1.start();
thread2.start();
}
}
对比synchronized
volatile相对于synchronized更轻量,如果一个共享变量至始至终只被各个线程赋值,而没有其他操作,那么就可以使用volatile来代替synchronized以及原子变量,因为赋值本身就具有原子性,而volatile又保证了可见性,所以就保证了线程安全
原子性
原子性:一系列操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分隔的
比如i++操作其实就是:取出i的值,加1,赋回来i的值,这是三步操作,不是原子性的,可使用synchronized实现原子行
原子性操作
- 除long和double之外的基本类型赋值操作都是原子操作
- 所有引用类型的赋值操作都是原子操作,无论是32位还是64位机器
java.concurrent.Atomic.*包中的所有类都是原子类
long和double的原子性
由于long和double是64位的值,所以在32位机器上会被视为两次单独的写入,每次写32位;64为位的JVM上是原子的
- Oracle官方鼓励JVM在实现时,避免拆分64位值
- Oracle官方鼓励程序员将其使用volatile修饰或进行同步,以防止JVM在实现时为实现上述内容
原子操作的组合
以下操作并不能一定保证线程安全
synchronized (this){
//...
}
synchronized (this){
//...
}
需要再套一个,来保证线程安全
synchronized (this){
synchronized (this){
//...
}
synchronized (this){
//...
}
}

Comments NOTHING