>  기사  >  Java  >  [Java 동시성 싸움]------Java 메모리 모델이 발생하기 전에

[Java 동시성 싸움]------Java 메모리 모델이 발생하기 전에

黄舟
黄舟원래의
2017-02-24 10:03:011253검색

지난 블로그([Deadly Java Concurrency] - 휘발성 구현 원리 심층 분석)에서 LZ는 스레드 로컬 메모리와 메인 메모리의 존재로 인해 재정렬과 결합되어 다중 스레드 환경 가시성 문제가 있습니다. 따라서 동기화와 잠금을 올바르게 사용한다면 스레드 A는 언제 스레드 B에 표시되는 변수 a를 수정합니까?

모든 시나리오에서 스레드에 의해 수정된 변수가 다른 스레드에 표시되는 시기를 지정할 수는 없지만, 이 규칙은 JDK 5부터 시작됩니다. 여러 스레드 간의 메모리 가시성을 설명하기 위해 이전 발생 개념을 사용합니다.

JMM에서 한 작업의 결과를 다른 작업에서 볼 수 있어야 하는 경우 두 작업 사이에 사전 발생 관계가 있어야 합니다.

데이터에 경쟁이 있는지, 스레드가 안전한지 여부를 판단하는 기본 원칙은 매우 중요합니다. 동시 환경에서 두 작업 간의 충돌이 발생합니다. 모든 질문입니다. 이전 발생에 대해 조금 알아보기 위해 간단한 예를 들어보겠습니다.

i = 1;       //线程A执行
j = i ;      //线程B执行

j는 1인가요? 스레드 A(i = 1)의 작업이 스레드 B(j = i)의 작업 이전에 발생한다고 가정하면 스레드 B의 실행 후에 j = 1이 참이어야 한다고 판단할 수 있습니다. 이러한 작업이 존재하지 않는 경우 사전 발생 원칙이 적용되면 j = 1이 되지 않습니다. 이 원칙이 확립되어야 합니다. 이것이 사전 발생 원칙의 힘입니다.

선행 원칙은 다음과 같이 정의됩니다.

1. 작업이 다른 작업 전에 발생하면 첫 번째 작업의 실행 결과가 두 번째 작업에 표시됩니다. 그리고 첫 번째 작업의 실행 순서는 두 번째 작업 이전입니다.
2. 두 작업 사이에는 사전 발생 관계가 있습니다. 이는 사전 발생 원칙에 따라 설정된 순서대로 실행되어야 한다는 의미는 아닙니다. 재정렬 후의 실행 결과가 이전 발생 관계에 따른 실행 결과와 일치하면 이러한 재정렬은 불법이 아닙니다.

다음은 사전 발생 원칙 규칙입니다.

  1. 프로그램 순서 규칙: 스레드 내에서는 코드 순서에 따라 작업이 작성됩니다. 쓰기 작업에서 앞부분이 먼저 발생합니다.

  2. 잠금 규칙: 나중에 동일한 잠금 작업 전에 잠금 해제 작업이 발생합니다.

  3. 휘발성 변수 규칙: 변수에 대한 쓰기 작업이 먼저 발생하고 이후 변수에 대한 읽기 작업이 발생합니다.

  4. 전송 규칙: 작업 A가 먼저 발생하면 작업 B가 먼저 발생하고 작업 B가 먼저 발생합니다. 작업 C의 경우 작업 A가 작업 C보다 먼저 발생한다고 결론을 내릴 수 있습니다.

  5. 스레드 시작 규칙: 이 스레드의 모든 작업에 대해 Thread 개체의 start() 메서드가 먼저 발생합니다. ;

  6. 스레드 중단 규칙: 중단된 스레드의 코드가 인터럽트 이벤트 발생을 감지하면 스레드 Interrupt() 메서드 호출이 먼저 발생합니다. 🎜>

    스레드 종료 규칙: 스레드의 모든 작업은 스레드가 종료될 때 먼저 발생합니다. Thread.join() 메서드의 끝과 Thread.isAlive의 반환 값을 통해 스레드가 실행을 종료했음을 감지할 수 있습니다. ();
  7. 객체 종료 규칙: 객체 초기화는 finalize() 메서드 시작 부분에서 먼저 발생합니다. 위의 각 규칙에 대한 자세한 내용("Java Virtual Machine에 대한 심층적인 이해, 12장"에서 발췌):
  8. 프로그램 순서 규칙

    : 코드 조각의 결과 단일 스레드에서 실행되는 순서가 지정됩니다. 가상머신과 프로세서가 명령어를 재정렬하기 때문에 이는 실행 결과라는 점에 유의하세요(재정렬에 대해서는 나중에 자세히 설명합니다). 순서가 바뀌더라도 프로그램의 실행 결과에는 영향을 미치지 않으므로 프로그램의 최종 실행 결과는 순차적 실행 결과와 일치합니다. 따라서 이 규칙은 단일 스레드에만 유효하며 다중 스레드 환경에서는 정확성을 보장할 수 없습니다.

잠금 규칙

: 이 규칙은 단일 스레드 환경이든 다중 스레드 환경이든 관계없이 잠금이 잠겨 있으면 먼저 잠금 해제 작업을 수행해야 합니다. 잠금 작업을 수행할 수 있습니다.

휘발성 변수 규칙

: 이는 휘발성이 스레드 가시성을 보장한다는 것을 나타내는 중요한 규칙입니다. 일반인의 관점에서 보면 스레드가 먼저 휘발성 변수를 쓴 다음 스레드가 변수를 읽는 경우 쓰기 작업은 읽기 작업 전에 발생해야 합니다.

전이적 규칙

: 전이 원칙이 전이적임을 보여줍니다. 즉, A는 B 전에 발생하고 B는 C 전에 발생하고 A는 C 전에 발생합니다

스레드 시작 규칙

: 스레드 A가 실행 중에 ThreadB.start()를 실행하여 스레드 B를 시작한다고 가정합니다. 그러면 스레드 A가 공유 변수를 수정하면 스레드 B가 스레드 B 다음에 수정됩니다. 실행이 시작됩니다.

스레드 종료 규칙

: 스레드 A가 실행 중에 ThreadB.join()을 공식화하여 스레드 B가 종료될 때까지 기다린 다음 종료 전에 스레드 B가 공유 변수를 수정하는 것은 다음과 같다고 가정합니다. 스레드 A는 반환 후 표시될 때까지 기다립니다.

위의 8가지 규칙은 기본 Java가 사전 발생 관계를 충족하는 규칙이지만, 이들로부터 사전 발생 관계를 충족하는 다른 규칙을 추론할 수 있습니다.

  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任务开始执行操作

这里再说一遍happens-before的概念:如果两个操作不存在上述(前面8条 + 后面6条)任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

下面就用一个简单的例子来描述下happens-before原则:

private int i = 0;public void write(int j ){
    i = j;
}public int read(){    return i;
}

我们约定线程A执行write(),线程B执行read(),且线程A优先于线程B执行,那么线程B获得结果是什么?;我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 + 推导的6条可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,所以肯定不满足程序次序规则;

  2. 两个方法都没有使用锁,所以不满足锁定规则;

  3. 变量i不是用volatile修饰的,所以volatile变量规则不满足;

  4. 传递规则肯定不满足;

所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但是就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?满足规则2、3任一即可。

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

下图是happens-before与JMM的关系图(摘自《Java并发编程的艺术》)
[Java 동시성 싸움]------Java 메모리 모델이 발생하기 전에

参考资料

  1. 周志明:《深入理解Java虚拟机》

  2. 方腾飞:《Java并发编程的艺术》

在上篇博客(【死磕Java并发】—–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题。那么我们正确使用同步、锁的情况下,线程A修改了变量a何时对线程B可见?

我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是happens-before,从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。

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

happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面我们就一个简单的例子稍微了解下happens-before ;

i = 1;       //线程A执行
j = i ;      //线程B执行

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。这就是happens-before原则的威力。

happens-before原则定义如下:

1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;

  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

我们来详细看看上面每条规则(摘自《深入理解Java虚拟机第12章》):

程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

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

线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

上面八条是原生Java满足Happens-before关系的规则,但是我们可以对他们进行推导出其他满足happens-before的规则:

  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任务开始执行操作

这里再说一遍happens-before的概念:如果两个操作不存在上述(前面8条 + 后面6条)任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

下面就用一个简单的例子来描述下happens-before原则:

private int i = 0;public void write(int j ){
    i = j;
}public int read(){    return i;
}

我们约定线程A执行write(),线程B执行read(),且线程A优先于线程B执行,那么线程B获得结果是什么?;我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 + 推导的6条可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,所以肯定不满足程序次序规则;

  2. 两个方法都没有使用锁,所以不满足锁定规则;

  3. 变量i不是用volatile修饰的,所以volatile变量规则不满足;

  4. 传递规则肯定不满足;

所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但是就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?满足规则2、3任一即可。

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

다음 그림은 이전과 JMM의 관계입니다("The Art of Java Concurrent 프로그래밍"에서 발췌)
[Java 동시성 싸움]------Java 메모리 모델이 발생하기 전에

위는 [Dead Java Concurrency]입니다. --- --Java 메모리 모델의 내용은 이전에 발생합니다. 더 많은 관련 내용은 PHP 중국어 웹사이트(www.php.cn)를 참고하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.