什么是Happens-before ?
1.)Happens-before 规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是Happens-before 原则。
2.)如果一个操作Happens-before 第二个操作,那么我们就说第一个操作对于第二个操作是可见的。
什么不是Happens-before ?
两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就是不具备happens-before。
happens-before规则有哪些?
1.)单线程规则
后面的语句一定能看见前面的代码的操作
2.)锁操作
A线程解锁了,B线程获得了同样的锁,那么B一定能看到A的所有操作
3.)volatile变量
多线程之间读取到volatile变量的所有操作。且与volatile变量有关的变量也能保证可见,volatile写入之前的操作,其余变量的可见。
4.)线程启动
子线程能看见主线程的所有操作
5.)线程的join
在B中执行A.join();A happens-before B 同4
6.) 具有传递性
hb(AB),hb(B,C),那么A hb C
7.)中断
一个线程被中断,其他线程一定能感知
8.) 并发工具类的happens-before 原则
a. 线程安全的容器get一定能看见在此之前的put操作。
b.CountDownLatch
c.Semaphore 获取许可证之前必须有人释放
d.CyclicBarrier 必须达到释放的条件
e. future get方法一定是拿到执行完后的结果
f.线程池中提交的任务,每一个任务都可以看到提前之前的所有结果。
volatile关键字
volatile 是一种比锁更加轻量的同步操作,仅仅是刷回主内存这个操作,不会发生上下文切换这种开销。如果一个变量被volatile修饰,JVM便会知道这个变量是可能被并发修改的,会进行一系列操作,比如进制重排序等等。
volatile开销小,责任也小,volatile做不大锁的原子性。
不适用的场景 a++
/**
* @author FuYouJ
* @date 2020-02-15 11:52
* volatile 不适合A++
*/
public class NoVolatile implements Runnable {
volatile int a=0;
AtomicInteger realA=new AtomicInteger();
@Override
public void run() {
for (int i = 0; i <10000 ; i++) {
a++;
realA.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r=new NoVolatile();
Thread thread1=new Thread(r);
Thread thread2=new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread1.join();
//小于200000
System.out.println("a="+((NoVolatile)r).a);
System.out.println("realA="+((NoVolatile)r).realA);
}
}
适合的场景1
类似于boolean flag 单纯的赋值操作不依赖原先的值(不适合消费者生产者模式)
这样的代码是可行的
flag=true;
a=6;
这样的代码是不可行的
flag=!flag;
a=a+1;
boolean的复制并不是一般的先读取,再改写,再回写3步操作,而是原子性的1步操作。
适合的场景2 作为刷新之前变量的触发器
那么以下代码在执行的时候,是所有线程可见的。
int a=1;
int ab=2;
volatile b=3;
总结一下,volatile的两点作用:
1.可见性:
读取一个变量之前,需要先使本地的缓存失效,这样就必须到主内存读取最新值,写一个volatile属性,会立即刷回到主内存。
2.禁止指令重排序:解决单例模式双重锁的乱序问题
volatile 和synchronized的关系?
如果一个共享变量只是被各个线程单纯的赋值,而没有其他的操作,那么这个时候局可以用volatile代替synchronized或者代替原子变量,因为赋值一步操作是具有原子性的,而volatile又保证了可见性,所以足以保证线程安全而不必要花费太大的开销(上下文切换和获取锁等等)。
volatile 可以让long double的赋值是原子性的
由于long和double是64位的值,单个数字的写入会被视为两个单独的写入,每次写32位,这可能会导致A线程读取的是前32位(改)后32位(未修改),B线程读取的是前32位(改)后32位(改)。
官方鼓励JVM(32位),不必要拆分64位写操作,鼓励程序员(我可能是)用volatile修饰变量。或者使用额外的同步
synchronized 的可见性
synchronized 不仅仅有保证原子性,还保证可见性。
例如用 syncronized代码块执行a++却不用volatile修饰,同样可以保证a对其他线程可见。
int a=0;
synchronized (this){
for(;;){
a++;}
}
符合上面的原则2,A线程解锁了,B线程获得了同样的锁,那么B一定能看到A的所有操作。
//定义变量
int a,b,c,d;
//线程A执行
void change(){
a=2;
b=3;
c=4;
synchronized (this){
d=5;
}
}
//线程B执行
void read(){
synchronzied(this){int aa=a}
bb=b;
cc=c;
dd=d;
}