>  기사  >  Java  >  Java의 동기화 키워드의 교착 상태 및 메모리 사용 문제에 대한 자세한 설명

Java의 동기화 키워드의 교착 상태 및 메모리 사용 문제에 대한 자세한 설명

高洛峰
高洛峰원래의
2017-01-05 16:10:341972검색

먼저 동기화에 대한 자세한 설명을 살펴보겠습니다.
synchronized는 Java 언어의 키워드입니다. 메소드나 코드 블록을 수정하는 데 사용되면 최대 하나의 스레드가 해당 코드를 실행하도록 보장할 수 있습니다. 동시.

1. 두 개의 동시 스레드가 동일한 객체 개체의 동기화된(this) 동기화 코드 블록에 액세스하는 경우 한 번에 하나의 스레드만 실행될 수 있습니다. 다른 스레드는 이 코드 블록을 실행하기 전에 현재 스레드가 이 코드 블록 실행을 완료할 때까지 기다려야 합니다.

2. 그러나 스레드가 객체의 동기화된(this) 동기화 코드 블록에 액세스하면 다른 스레드는 여전히 객체의 동기화되지 않은(this) 동기화 코드 블록에 액세스할 수 있습니다.

3. 특히 중요한 점은 스레드가 객체의 동기화된(this) 동기화 코드 블록에 액세스할 때 다른 스레드가 객체의 다른 모든 동기화된(this) 동기화 코드 블록에 액세스하는 것이 차단된다는 것입니다.

4. 세 번째 예는 다른 동기화 코드 블록에도 적용 가능합니다. 즉, 스레드가 개체의 동기화된(this) 동기화 코드 블록에 액세스하면 이 개체의 개체 잠금을 획득합니다. 결과적으로 개체 개체의 모든 동기화된 코드 부분에 대한 다른 스레드의 액세스가 일시적으로 차단됩니다.

5. 위의 규칙은 다른 객체 잠금에도 적용 가능합니다.
간단히 말해서 동기화는 현재 스레드에 대한 잠금을 선언하는 것입니다. 및 기타 스레드 잠금을 획득한 후 동일한 작업을 수행하면 됩니다.
이것은 매우 유용하지만 작성자는 또 다른 이상한 상황에 직면했습니다.
1. 동기화된 키워드 선언 사용
2. 메서드 중 하나가 실행되면 다른 메서드(비동기 스레드 콜백)가 실행될 때까지 기다려야 하므로 countDownLatch를 사용하여 대기합니다.
3.

synchronized void a(){
 countDownLatch = new CountDownLatch(1);
 // do someing
 countDownLatch.await();
}
 
synchronized void b(){
   countDownLatch.countDown();
}

그 중
메소드는 메인 스레드에 의해 실행되고, 메소드 b는 비동기 스레드에 의해 실행된 후 다시 호출됩니다
. > 메소드 a를 실행한 후 메인 스레드가 멈추기 시작하고 더 이상 진행되지 않습니다. 얼마나 오래 기다려도 상관 없습니다.
이것은 매우 고전적인 교착 상태 문제입니다
a가 b의 실행을 기다리고 있습니다. 사실, b를 콜백으로 보지 마세요. b도 a가 실행되기를 기다리고 있습니다.
일반적으로 코드 블록을 동기화하려면 공유 변수를 사용하여 잠가야 합니다.

byte[] mutex = new byte[0];
 
void a1(){
   synchronized(mutex){
     //dosomething
   }
}
 
void b1(){
 
   synchronized(mutex){
     // dosomething
   }
 
}
메소드 a와 메소드 b의 내용이 분리된 경우 a1과 b1 메소드의 동기화된 블록으로 마이그레이션하는 것은

a1이 실행된 후에는 이해하기 쉽습니다. b1 메소드가 실행될 때까지 간접적으로 대기(countDownLatch)합니다.
그러나 a1의 뮤텍스가 해제되지 않았으므로 이때는 b1 메소드에 대한 비동기 콜백이라 하더라도 b1을 대기하기 시작합니다. 뮤텍스가 잠금을 해제할 때까지 기다려야 하면 b 메서드는 실행되지 않습니다.
이로 인해 교착 상태가 발생합니다!
여기서 동기화된 키워드는 메소드 앞에 배치될 때 동일한 효과를 가집니다. 단지 Java 언어가 뮤텍스의 선언과 사용을 숨기는 데 도움이 된다는 것뿐입니다. 동일한 객체에서 동기화된 메소드가 사용하는 뮤텍스는 동일합니다. 따라서 비동기 콜백에서도 교착상태가 발생할 수 있으므로 주의하시기 바랍니다. 이 정도의 오류는 동기화된 키워드를 잘못 사용했기 때문에 발생합니다. 무분별하게 사용하지 말고
그런 보이지 않는 뮤텍스 개체를 올바르게 사용하세요. 정확히 무엇인가요?
인스턴스 자체를 생각하면 쉽습니다. 이렇게 하면 이 아이디어를 증명하기 위해 새로운 객체를 정의하고 잠글 필요가 없기 때문입니다.
아이디어는 매우 간단합니다. 클래스에는 두 개의 메소드가 있습니다. 하나의 메소드는 동기화로 선언되고, 하나는 메소드 본문에서 동기화(this)를 사용한 다음 두 스레드를 시작하여 각각 잠금 경쟁인 경우 이 두 메소드를 호출합니다. 두 메서드(wait) 사이에 발생하면 동기화된 메서드 선언의 보이지 않는 뮤텍스가 실제로 인스턴스 자체라고 설명할 수 있습니다.

public class MultiThreadSync {
 
  public synchronized void m1() throws InterruptedException{
     System. out.println("m1 call" );
     Thread. sleep(2000);
     System. out.println("m1 call done" );
  }
 
  public void m2() throws InterruptedException{
     synchronized (this ) {
       System. out.println("m2 call" );
       Thread. sleep(2000);
       System. out.println("m2 call done" );
     }
  }
 
  public static void main(String[] args) {
     final MultiThreadSync thisObj = new MultiThreadSync();
 
     Thread t1 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m1();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     Thread t2 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m2();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     t1.start();
     t2.start();
 
  }
 
}
결과 출력은 다음과 같습니다.

m1 call
m1 call done
m2 call
m2 call done
메소드 m2의 sync 블록이 대기한다는 설명 이렇게 하면 위의 가정을 확인할 수 있습니다.

또한 static 메소드에 sync를 추가하면 클래스 수준 메소드이므로 잠김이 발생한다는 점에도 유의해야 합니다. 객체는 현재 클래스의 예입니다. 이를 증명하는 프로그램을 작성할 수도 있습니다. 여기서는 생략합니다.
따라서 읽을 때 메서드의 동기화된 키워드가 자동으로 동기화(this){}로 대체될 수 있습니다. 이해하기 쉽습니다.

                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }
동기화의 메모리 가시성부터 시작하겠습니다

Java에서 동기화 키워드를 사용하여 스레드 간 상호 배제를 구현할 수 있다는 것을 모두 알고 있지만 종종 잊어버립니다. 여기에는 또 다른 기능이 있습니다. 즉, 메모리에 있는 변수의 가시성을 보장하는 것입니다. 즉, 읽기 및 쓰기 두 스레드가 동시에 동일한 변수에 액세스할 때 쓰기 스레드가 변수를 업데이트한 후 동기화를 사용하여 읽기 스레드는 변수에 다시 액세스할 때 변수의 최신 값을 읽을 수 있습니다.

예를 들어 다음 예는 다음과 같습니다.

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      while (!ready) {
        Thread.yield(); //交出CPU让其它线程工作
      }
      System.out.println(number);
    }
  }
 
  public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
  }
}
읽기 스레드가 무엇을 출력할 것이라고 생각하시나요? 42? 정상적인 상황에서는 42가 출력됩니다. 그러나 재정렬 문제로 인해 읽기 스레드가 0을 출력하거나 아무것도 출력하지 않을 수 있습니다.

우리는 Java 코드를 바이트코드로 컴파일할 때 컴파일러가 코드 순서를 변경할 수 있고, 기계 명령을 실행할 때 CPU도 명령 순서를 변경할 수 있다는 것을 알고 있습니다. 정렬은 프로그램의 의미를 손상시키지 않습니다.

在单一线程中,只要重排序不会影响到程序的执行结果,那么就不能保证其中的操作一定按照程序写定的顺序执行,即使重排序可能会对其它线程产生明显的影响。
这也就是说,语句"ready=true"的执行有可能要优先于语句"number=42"的执行,这种情况下,读线程就有可能会输出number的默认值0.

而在Java内存模型下,重排序问题是会导致这样的内存的可见性问题的。在Java内存模型下,每个线程都有它自己的工作内存(主要是CPU的cache或寄存器),它对变量的操作都在自己的工作内存中进行,而线程之间的通信则是通过主存和线程的工作内存之间的同步来实现的。

比如说,对于上面的例子而言,写线程已经成功的将number更新为42,ready更新为true了,但是很有可能写线程只同步了number到主存中(可能是由于CPU的写缓冲导致),导致后续的读线程读取的ready值一直为false,那么上面的代码就不会输出任何数值。

而如果我们使用了synchronized关键字来进行同步,则不会存在这样的问题,

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
  private static Object lock = new Object();
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      synchronized (lock) {
        while (!ready) {
          Thread.yield();
        }
        System.out.println(number);
      }
    }
  }
 
  public static void main(String[] args) {
    synchronized (lock) {
      new ReaderThread().start();
      number = 42;
      ready = true;
    }
  }
}

这个是因为Java内存模型对synchronized语义做了以下的保证,

即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

这实际上是JSR133定义的其中一条happen-before规则。JSR133给Java内存模型定义以下一组happen-before规则,

单线程规则:同一个线程中的每个操作都happens-before于出现在其后的任何一个操作。

对一个监视器的解锁操作happens-before于每一个后续对同一个监视器的加锁操作。

对volatile字段的写入操作happens-before于每一个后续的对同一个volatile字段的读操作。

Thread.start()的调用操作会happens-before于启动线程里面的操作。

一个线程中的所有操作都happens-before于其他线程成功返回在该线程上的join()调用后的所有操作。

一个对象构造函数的结束操作happens-before与该对象的finalizer的开始操作。

传递性规则:如果A操作happens-before于B操作,而B操作happens-before与C操作,那么A动作happens-before于C操作。

实际上这组happens-before规则定义了操作之间的内存可见性,如果A操作happens-before B操作,那么A操作的执行结果(比如对变量的写入)必定在执行B操作时可见。

为了更加深入的了解这些happens-before规则,我们来看一个例子:

//线程A,B共同访问的代码
Object lock = new Object();
int a=0;
int b=0;
int c=0;
 
//线程A,调用如下代码
synchronized(lock){
  a=1; //1
  b=2; //2
} //3
c=3; //4
 
 
//线程B,调用如下代码
synchronized(lock){ //5
  System.out.println(a); //6
  System.out.println(b); //7
  System.out.println(c); //8
}

我们假设线程A先运行,分别给a,b,c三个变量进行赋值(注:变量a,b的赋值是在同步语句块中进行的),然后线程B再运行,分别读取出这三个变量的值并打印出来。那么线程B打印出来的变量a,b,c的值分别是多少?

根据单线程规则,在A线程的执行中,我们可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B线程的执行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根据监视器的解锁和加锁原则,3操作(解锁操作)是happens before 5操作的(加锁操作),再根据传递性 规则我们可以得出,操作1,2是happens before 操作6,7,8的。

则根据happens-before的内存语义,操作1,2的执行结果对于操作6,7,8是可见的,那么线程B里,打印的a,b肯定是1和2. 而对于变量c的操作4,和操作8. 我们并不能根据现有的happens before规则推出操作4 happens before于操作8. 所以在线程B中,访问的到c变量有可能还是0,而不是3.


更多详解Java中synchronized关键字的死锁和内存占用问题相关文章请关注PHP中文网!


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