0
点赞
收藏
分享

微信扫一扫

happens-before简介

彪悍的鼹鼠 2022-04-19 阅读 86
java

并发编程三个特性
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程中不会被任何因素打断,要么就都不执行

int i = 0;
int j = i;
i++;
i = j + 1;

1:java中,对基本数据类型的变量和复制操作都是原子性操作;

2:包含了两个操作,读取i,将i值赋值给j;

3:读取i,计算i+1,将i+1的值赋值给i;

4:同3一样。

如果上述程序是在单线程的环境下进行的,那么我们可以认为整个步骤都是原子性操作;在多线程的环境下,java只保证了基本数据类型的变量和复制操作才是原子性的(32位的jdk环境下,对64位数据的读取不是原子操作,如long、double),可通过synchronized来保证原子性

可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现的

指令重排序原则:保证不影响代码执行语义的前提下,实现重排序,从而提升运行效率。当然对指令的重排序要严格遵守指令之间的数据依赖关系,并不是可以任意进行重排序的。

由此引申出happen-before原则

private static long count = 0;

private static void addCount() {
int idx = 0;
for (; idx < 10000; idx++) {
count++;
}
}

public static long calc() throws InterruptedException {

Thread th1 = new Thread(() -> {
    addCount();
});
Thread th2 = new Thread(() -> {
    addCount();
});
th1.start();
th2.start();
th1.join();
th2.join();

return count;

}

public static void main(String[] args) throws InterruptedException {
System.out.println(calc());
}

输出出的数字是一个10000-20000之间的随机数

th1的结果对th2是不可见的,反过来同样

有序性:即程序执行的顺序按照代码的先后顺序执行。

java内存模型中,为了效率,允许编译器和处理器对指令进行重排序,不影响单线程环境下的运行结果,对多线程有影响

在jmm中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系

它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题

1、程序次序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行

2、监视器锁规则:在监视器锁上的解锁操作,必须在同一个监视器锁上的加锁操作之前执行(同一个锁)

针对同一个锁m来说,A线程中的操作对B线程是可见的

3、volatile变量规则:对volatile变量的写入操作必须在对于该变量的读操作之前执行

4、线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

5、线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行。或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false

Thread th1 = new Thread(() -> {
count++;
});
Thread th2 = new Thread(() -> {
count++;
th1.start();
});
th2.start();
th2.join();
th1.join();
System.out.println(count);
6、中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)

一个线程被其他线程interrupt了, 那么检测中断(isInterrupted) 或者抛出InterruptedException一定看得到. 假设没有happens-before 原则, 那么可能检测是否中断的状态是不对的

7、终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成

8、传递性:操作A happens-before 操作B 并且操作B happens-before 操作C 则操作A happens-before 操作C。

java语言无需任何同步手段保障,就能成立的happens-before规则只有以上这些。

一个例子:

private int value;

public int getValue() {
return value;
}

public void setValue(int value) {
this.value = value;
}

public static void main(String[] args) throws InterruptedException {

TestClass testClass = new TestClass();

Thread th1 = new Thread(() -> {
testClass.setValue(1);
});

Thread th2 = new Thread(() -> {
System.out.println(testClass.getValue());
});
th1.start();
th2.start();
}

get和set方法,分别有两个线程调用,不在一个线程中,所以程序次序规则不适用;

没有同步代码块,不符合发生lock和unlock操作,线程锁定规则不适用;

value变了没有被volatile关键字修饰,volatile变量的规则不适用;

线程启动、终止、中断规则不适用;同样传递性也不存在,因此可以判断,虽然th1在操作时间上先于th2,但是th1的操作对th2的是不可见的,换句话说,这里面的操作不是线程安全的。

修复问题:

1、把get和set方法都定义为synchronize方法,这样可以套用监视器锁规则(此时锁住的是当前对象)

2、把value定义为volatile变量,由于set方法对value的修改不依赖value的原值,满足volatile关键字使用场景。

一个操作“时间上的先发生”,不代表这个操作会是“先行发生”。

同样,如果一个操作“现行发生”,也不能推导出这个操作必定是“时间上的先发生”。

指令重排序:

int i=1;
int j=2;
单线程中,根据程序次序规则,1的操作happens-before 2,但是2的代码完全可能先被处理器执行,不影响happens-before发生原则的正确性。

如果两个操作之前缺乏上述happens-before关系,那么jvm可以对它们任意的重排序

扩展:

1、将一个元素放入一个线程安全的队列的操作Happens-Before从队列中取出这个元素的操作

2、将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作

3、在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作

4、释放Semaphore许可的操作Happens-Before获得许可操作

5、Future表示的任务的所有操作Happens-Before Future#get()操作

6、向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作

举报

相关推荐

0 条评论