자바에서의 스레드 안전성은 매우 중요한 주제입니다. 자바는 자바 스레드를 사용하여 다중 스레드 환경을 지원합니다. 동일한 객체에서 생성된 여러 스레드가 객체 변수를 공유하고, 이는 스레드가 공유 데이터를 읽고 업데이트할 때 데이터 불일치로 이어질 수 있습니다.
스레드 안전성
데이터 불일치의 이유는 어떤 필드 값을 업데이트하는 것이 원자적인 프로세스가 아니기 때문입니다. 이를 위해서는 현재 값을 읽는 첫 번째 단계, 업데이트된 값을 얻기 위한 필요한 작업을 수행하는 두 번째 단계 및 업데이트된 값을 필드 참조에 할당하는 세 번째 단계가 필요합니다. 여러 스레드가 공유 데이터를 업데이트하는 간단한 프로그램으로 이를 확인해 봅시다.
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();
//스레드가 처리를 마칠 때까지 기다립니다
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) {
// 일부 작업 처리
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
위 프로그램의 for 루프에서, 카운트는 1씩 증가되고 두 개의 스레드가 있으므로 두 스레드가 실행을 마친 후에는 그 값이 8이어야 합니다. 그러나 위 프로그램을 여러 번 실행하면 카운트 값이 6,7,8 사이에서 변하는 것을 알 수 있습니다. 이는 카운트++가 원자적인 작업처럼 보이지만 사실 그렇지 않아 데이터 손상을 일으키기 때문입니다.
자바에서의 스레드 안전성
자바에서의 스레드 안전성은 프로그램을 다중 스레드 환경에서 안전하게 사용하기 위한 프로세스입니다. 프로그램을 스레드 안전하게 만들기 위해 다양한 방법이 있습니다.
- 동기화는 자바에서 스레드 안전성을 위한 가장 쉽고 널리 사용되는 도구입니다.
- java.util.concurrent.atomic 패키지의 Atomic Wrapper 클래스 사용 예시. 예: AtomicInteger
- java.util.concurrent.locks 패키지에서 락 사용.
- 스레드 안전한 컬렉션 클래스 사용. 스레드 안전성을 위해 ConcurrentHashMap 사용에 대한 이 게시물을 확인하십시오.
- 변수에 대해 volatile 키워드 사용하여 모든 스레드가 데이터를 메모리에서 읽고 스레드 캐시에서 읽지 않도록 함.
자바 동기화
동기화는 스레드 안전성을 달성할 수 있는 도구로, JVM은 동기화된 코드가 한 번에 한 스레드에 의해 실행될 것을 보장합니다. java 키워드 synchronized는 동기화된 코드를 생성하기 위해 사용되며 내부적으로 객체나 클래스에 대한 락을 사용하여 한 번에 한 스레드만 동기화된 코드를 실행하도록 합니다.
- 자바 동기화는 스레드가 동기화된 코드에 진입하기 전에 리소스를 잠금 상태로 만들고, 코드 실행이 끝나면 다른 스레드가 잠금을 획득할 수 있도록 리소스를 잠금 해제합니다. 그 동안 다른 스레드는 동기화된 리소스를 잠그기 위해 대기 상태에 있습니다.
- 우리는 synchronized 키워드를 두 가지 방법으로 사용할 수 있습니다. 하나는 메서드 전체를 동기화하는 것이고, 다른 하나는 동기화된 블록을 생성하는 것입니다.
- 메서드가 동기화되면, Object가 잠깁니다. 메서드가 정적이면 Class가 잠깁니다. 그래서 동기화가 필요한 메서드의 일부분만 잠그기 위해 동기화된 블록을 사용하는 것이 항상 좋은 방법입니다.
- 동기화된 블록을 생성할 때는 잠금을 획득할 리소스를 제공해야 합니다. XYZ.class나 클래스의 어떤 객체 필드든 될 수 있습니다.
synchronized(this)
는 동기화된 블록에 진입하기 전에 객체를 잠그게 됩니다.- 가능한 한 가장 낮은 수준의 잠금을 사용해야 합니다. 예를 들어, 클래스에 여러 동기화된 블록이 있는 경우 그 중 하나가 객체를 잠그면 다른 동기화된 블록들도 다른 스레드에 의해 실행될 수 없게 됩니다. 객체를 잠그면 객체의 모든 필드에 대해 잠금을 획득합니다.
- 자바 동기화는 성능의 대가로 데이터 무결성을 제공하므로, 절대로 필요하지 않은 경우에만 사용해야 합니다.
- 자바 동기화는 동일한 JVM에서만 작동하므로, 여러 JVM 환경에서 어떤 리소스를 잠그려면 작동하지 않고 전역 잠금 메커니즘을 사용해야 할 수도 있습니다.
- 자바 동기화는 데드락을 유발할 수 있습니다. 자바에서의 데드락 및 방지 방법에 대한 이 게시물을 확인하세요.
- 자바 동기화 키워드는 생성자 및 변수에 사용할 수 없습니다.
- 동기화 블록에 대해 사용할 더미 개인 객체를 만드는 것이 좋습니다. 이렇게 하면 다른 코드에서 참조를 변경할 수 없습니다. 예를 들어, 동기화되는 객체에 대한 setter 메서드가 있다면 다른 코드에서 참조를 변경할 수 있어 동기화된 블록이 병렬 실행될 수 있습니다.
- 상수 풀에 유지되는 객체를 사용해서는 안 됩니다. 예를 들어, 문자열은 동기화에 사용되어서는 안 됩니다. 동일한 문자열에 대해 잠그는 다른 코드가 있으면 동일한 참조 객체를 가져오려고 할 것이며, 코드가 관련이 없더라도 서로 잠길 것입니다. 문자열 풀에서 동일한 참조 객체에 대해 잠금을 시도할 것입니다.
위의 프로그램에서 수행해야 할 코드 변경 사항은 다음과 같습니다. 스레드 안전하게 만들기 위해.
//동기화를 위한 더미 객체 변수
private Object mutex=new Object();
...
//동기적으로 count 값을 읽고 증가시키고 업데이트하는 동기화 블록 사용
synchronized (mutex) {
count++;
}
동기화 예제를 살펴보고 어떤 점을 배울 수 있는지 알아봅시다.
public class MyObject {
// 객체의 모니터에 잠금
public synchronized void doSomething() {
// ...
}
}
// 해커 코드
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// 내 객체를 무기한 지연
Thread.sleep(Integer.MAX_VALUE);
}
}
주의하세요. 해커의 코드가 myObject 인스턴스를 잠그려고 시도하고 한 번 잠그면 해제하지 않으므로 doSomething() 메서드가 잠금을 기다리는 동안 블록됩니다. 이로 인해 시스템이 교착 상태에 빠지고 서비스 거부 (DoS)를 유발할 수 있습니다.
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
//신뢰할 수 없는 코드
MyObject myObject = new MyObject();
//잠금 개체 참조 변경
myObject.lock = new Object();
잠금 개체가 공개되어 있고 참조를 변경함으로써 동시에 여러 스레드에서 동기화된 블록을 실행할 수 있습니다. 비슷한 경우는 개인 개체가 있지만 참조를 변경할 수 있는 setter 메서드가 있는 경우에도 참입니다.
public class MyObject {
//클래스 개체의 모니터에 잠금
public static synchronized void doSomething() {
// ...
}
}
// 해커 코드
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();
//모든 스레드 시작
t1.start();t2.start();t3.start();
//스레드 종료 대기
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//공유 변수 값 확인
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++){
//데이터 처리 및 스레드 이름 추가
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
// 일부 작업 처리
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]
{
“error”: “Upstream error…”
}
Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java