Java内存模型中的happens-before是什么?为什么会有这东西的存在?
定义:
1、如果一个操作happens-before(之前发生)另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2、两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
并发Bug的源头就是线程之间的可见性,所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。
例如单例双重检测中,对象要用volatile 就是为了防止指令重排。可能会先关联对象应用关系,再初始化对象。这就会导致多线程情况下,报错,找不到对象。
1、程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
2、锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;论是单线程还是多线程,必须要先释放锁,然后其他线程才能进行lock操作
3、volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5、线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
6、线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
7、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
8、对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
volatile
变量在多个线程之间可见,但是不保证原子性
变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
package com.jinglitong.shop.controller;
class ThreadVolatileDemo extends Thread {
public volatile boolean flag = true;
@Override
public void run() {
System.out.println("开始执行子线程....");
while (flag) {
}
System.out.println("线程停止");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class ThreadVolatile {
/**
*
* 功能说明: 当没有加volatile的时候,
* 子线程flag = true;
* 当主线程修改flag = false的时候,
* 子线程的flag没有被修改,还是用
* 的私有内存数据,导致程序一直处于while循环中,
* 加上volatile之后flag值变了,
* 私有内存数据失效,得从主内存读取最新数据,
* 所以循环结束。
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setRuning(false);
System.out.println("flag 已经设置成false");
System.out.println(threadVolatileDemo.flag);
}
}
Volatile非原子性
注意: Volatile非原子性
package com.jinglitong.shop.controller;
public class VolatileNoAtomic extends Thread {
private static volatile int count;//static 放在静态区,只会放一次,所有线程共享
// private static AtomicInteger count = new AtomicInteger(0);
private static void addCount() {
for (int i = 0; i < 1000; i++) {
count++;
// count.incrementAndGet();
}
System.out.println(count);
}
public void run() {
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
Volatile可以保证可见性,有序性。其中可见性是指缓存修改完之后再刷到主内存的时候会让其他线程的缓存失效。没刷到主内存之前是不能可见的。A拿到count = 10,B也拿到10,A+1 = 11,这时候还没有刷到主内存中。B也+1 = 11,然后主内存count就等于11了。这就没法保证原子性了。
volatile使用的场景:最好是那种只有一个线程修改变量,多个线程读取变量的地方,对内存可见性高原子性低的地方。
volatile与枷锁机制的区别:枷锁机制确保了可见性和原子性,而volatile只有可见性。
volatile与synchronized区别
仅靠volatile不能保证线程的安全性。(原子性)
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
线程安全性
线程安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
ThreadLocal
多线程下的单例模式
单例模式本应该是在设计模式中讲的,也是设计模式中最简单的一种模式。在高并发下传统柜的设计模式就不行了,在这提供2中高并发设计下的单例模式。双检测和类中类(double check ;class in class)
public class Singletion {
private static class InnerSingletion {
private static Singletion single = new Singletion();
}
public static Singletion getInstance() {
return InnerSingletion.single;
}
}
package com.jinglitong.shop.controller;
public class DubbleSingleton {
private static DubbleSingleton ds;
public static DubbleSingleton getDs() {
if (ds == null) {
try {
// 模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (DubbleSingleton.class) {
if (ds == null) {
ds = new DubbleSingleton();
}
}
}
return ds;
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
}, "t3");
t1.start();
t2.start();
t3.start();
}
}