웹사이트 검색

Java의 스레드 안전성


Java의 스레드 안전은 매우 중요한 주제입니다. Java는 Java 스레드를 사용하여 다중 스레드 환경 지원을 제공합니다. 동일한 개체에서 생성된 여러 스레드가 개체 변수를 공유하므로 스레드가 공유 데이터를 읽고 업데이트하는 데 사용될 때 데이터 불일치가 발생할 수 있습니다.

스레드 안전성

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //wait for threads to finish processing
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // processing some job
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

위 프로그램 for 루프에서 count는 1씩 4번 증가하고 두 개의 스레드가 있으므로 두 스레드가 모두 실행을 마친 후에 값은 8이어야 합니다. 그러나 위의 프로그램을 여러 번 실행하면 카운트 값이 6,7,8 사이에서 변하는 것을 알 수 있습니다. 이는 count++이 원자적 연산인 것처럼 보이지만 데이터 손상을 일으키지 않기 때문에 발생합니다.

Java의 스레드 안전성

Java의 스레드 안전성은 다중 스레드 환경에서 프로그램을 안전하게 사용할 수 있도록 하는 프로세스이며 프로그램을 스레드로부터 안전하게 만들 수 있는 여러 가지 방법이 있습니다.

  • 동기화는 Java에서 스레드 안전을 위해 가장 쉽고 널리 사용되는 도구입니다.
  • java.util.concurrent.atomic 패키지의 Atomic Wrapper 클래스 사용. 예를 들어 AtomicInteger
  • java.util.concurrent.locks 패키지의 잠금 사용.
  • 스레드 안전 컬렉션 클래스를 사용하여 스레드 안전을 위한 ConcurrentHashMap 사용에 대해 이 게시물을 확인하십시오.
  • 변수와 함께 volatile 키워드를 사용하여 모든 스레드가 스레드 캐시에서 읽지 않고 메모리에서 데이터를 읽도록 합니다.

자바 동기화

동기화는 스레드 안전성을 달성할 수 있는 도구이며, JVM은 동기화된 코드가 한 번에 하나의 스레드에서만 실행되도록 보장합니다. java 키워드 synchronized는 동기화된 코드를 생성하는 데 사용되며 내부적으로 객체 또는 클래스에 대한 잠금을 사용하여 하나의 스레드만 동기화된 코드를 실행하도록 합니다.

  • 자바 동기화는 스레드가 동기화된 코드에 들어가기 전에 리소스의 잠금 및 잠금 해제 작업을 수행하며 개체에 대한 잠금을 획득해야 하며 코드 실행이 종료되면 다른 스레드에서 잠글 수 있는 리소스의 잠금을 해제합니다. 그 동안 다른 스레드는 동기화된 리소스를 잠그기 위해 대기 상태에 있습니다.
  • synchronized 키워드는 두 가지 방법으로 사용할 수 있습니다. 하나는 전체 메서드를 동기화하는 것이고 다른 하나는 동기화된 블록을 만드는 것입니다.
  • 메서드가 동기화되면 객체를 잠그고, 메서드가 정적인 경우 클래스를 잠그므로 항상 동기화 블록을 사용하여 동기화가 필요한 메서드 섹션만 잠그는 것이 가장 좋습니다.
  • 동기화된 블록을 생성하는 동안 잠금을 획득할 리소스를 제공해야 합니다. XYZ.class 또는 클래스의 Object 필드가 될 수 있습니다.
  • synchronized(this)는 동기화된 블록에 들어가기 전에 개체를 잠급니다.
  • 가장 낮은 수준의 잠금을 사용해야 합니다. 예를 들어 클래스에 동기화된 블록이 여러 개 있고 그 중 하나가 개체를 잠그면 다른 동기화된 블록도 사용할 수 없습니다. 다른 스레드에 의한 실행. 개체를 잠그면 개체의 모든 필드가 잠깁니다.
  • Java 동기화는 성능 비용에 대한 데이터 무결성을 제공하므로 꼭 필요한 경우에만 사용해야 합니다.
  • Java 동기화는 동일한 JVM에서만 작동하므로 여러 JVM 환경에서 일부 리소스를 잠글 필요가 있는 경우 작동하지 않으며 일부 전역 잠금 메커니즘을 살펴봐야 할 수도 있습니다.
  • Java 동기화로 인해 교착 상태가 발생할 수 있습니다. Java의 교착 상태 및 이를 방지하는 방법에 대한 이 게시물을 확인하십시오.
  • 자바 동기화 키워드는 생성자와 변수에 사용할 수 없습니다.
  • 동기화된 블록에 사용할 더미 개인 개체를 생성하여 참조가 다른 코드에 의해 변경될 수 없도록 하는 것이 좋습니다. 예를 들어 동기화 중인 개체에 대한 setter 메서드가 있는 경우 동기화된 블록의 병렬 실행으로 이어지는 다른 코드에 의해 참조가 변경될 수 있습니다.
  • 상수 풀에서 유지 관리되는 개체를 사용해서는 안 됩니다. 예를 들어 다른 코드도 동일한 문자열에 잠그고 있는 경우 동기화에 문자열을 사용해서는 안 됩니다. 문자열 풀과 두 코드가 관련이 없더라도 서로를 잠급니다.

다음은 스레드로부터 안전하게 만들기 위해 위의 프로그램에서 수행해야 하는 코드 변경 사항입니다.

    //dummy object variable for synchronization
    private Object mutex=new Object();
    ...
    //using synchronized block to read, increment and update count value synchronously
    synchronized (mutex) {
            count++;
    }

몇 가지 동기화 예와 이를 통해 무엇을 배울 수 있는지 살펴보겠습니다.

public class MyObject {
 
  // Locks on the object's monitor
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Indefinitely delay myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

해커의 코드는 myObject 인스턴스를 잠그려고 시도하고 일단 잠금을 얻으면 잠금을 해제하지 않아 doSomething() 메서드가 잠금을 기다리는 동안 차단되고 이로 인해 시스템이 교착 상태에 빠지고 서비스 거부(Denial of Service)가 발생합니다. 도스).

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//untrusted code

MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();

잠금 개체가 공개되어 있고 해당 참조를 변경하면 여러 스레드에서 동기화된 블록을 병렬로 실행할 수 있습니다. 개인 개체가 있지만 해당 참조를 변경하는 setter 메서드가 있는 경우에도 비슷한 경우가 있습니다.

public class MyObject {
  //locks on the class object's monitor
  public static synchronized void doSomething() { 
    // ...
  }
}
 
// hackers code
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

해커 코드가 클래스 모니터에 잠금을 설정하고 해제하지 않으면 시스템에서 교착 상태와 DoS가 발생합니다. 다음은 여러 스레드가 동일한 문자열 배열에서 작업하고 일단 처리되면 스레드 이름을 배열 값에 추가하는 또 다른 예입니다.

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //start all the threads
        t1.start();t2.start();t3.start();
        //wait for threads to finish
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //check the shared variable value now
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //process data and append thread name
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // processing some job
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

위의 프로그램을 실행했을 때의 결과는 다음과 같습니다.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

공유 데이터 및 동기화 없음으로 인해 문자열 배열 값이 손상되었습니다. 프로그램을 스레드로부터 보호하기 위해 addThreadName() 메서드를 변경하는 방법은 다음과 같습니다.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

이 변경 후 프로그램이 제대로 작동하고 여기에 프로그램의 올바른 출력이 있습니다.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

이것이 자바의 스레드 안전을 위한 전부입니다. 스레드 안전 프로그래밍과 동기화된 키워드 사용에 대해 배웠기를 바랍니다.