자바에서 멀티스레드를 이용하면 여러작업을 동시에 처리할 수 있기 때문에 작업효율이 좋아집니다. 하지만 하나의 공유자원을 여러 스레드에서 동시에 접근하여 사용하게 되면 때때로 우리가 예상치 못한 결과가 나타나게 됩니다.
이를 해결하기 위해 스레드 동기화를 사용합니다.
스레드 동기화는 멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는것을 말합니다. 공유데이터가 사용되어 동기화가 필요한 부분을 임계영역(critical section)이라고 부르며, 자바에서는 이 임계영역에 synchronized 키워드를 사용하여 여러 스레드가 동시에 접근하는 것을 금지함으로써 동기화를 할 수 있습니다.
멀티 스레드를 사용하여 개발 중 동기화가 필요해 synchronized를 사용하였는데 어떤 상황에서 각각의 스레드가 lock이 걸리는지 알아보고자 쓴 글입니다.
하나의 인스턴스, 서로 다른 스레드가 실행
public class SynchronizedMethod {
public static void main(String[] args) {
SynchronizedMethod sync = new SynchronizedMethod();
Thread thread1 = new Thread(() -> {
System.out.println("스레드 1 시작 : " + LocalDateTime.now());
sync.syncMethod1("스레드 1");
System.out.println("스레드 1 종료 : " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드 2 시작 : " + LocalDateTime.now());
sync.syncMethod2("스레드 2");
System.out.println("스레드 2 종료 : " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
객체의 인스턴스를 하나 생성하고 두 개의 스레드를 만들어 각각 synchronized 키워드가 붙은 메소드를 실행했다.
<결과>
스레드 1 시작 : 2023-09-22T16:51:35.957619300
스레드 2 시작 : 2023-09-22T16:51:35.957619300
스레드 1의 syncMethod1 실행중2023-09-22T16:51:35.961132900
스레드 1 종료 : 2023-09-22T16:51:40.968256100
스레드 2의 syncMethod2 실행중2023-09-22T16:51:40.968256100
스레드 2 종료 : 2023-09-22T16:51:45.983554300
스레드 1의 syncMethod1()이 종료된 다음, 스레드 2가 syncMethod2()를 실행하는 것을 확인할 수 있다.
각각의 인스턴스들이 스레드 실행
package org.example;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class SynchronizedMethod {
public static void main(String[] args) {
SynchronizedMethod sync = new SynchronizedMethod();
SynchronizedMethod sync2 = new SynchronizedMethod();
Thread thread1 = new Thread(() -> {
System.out.println("스레드 1 시작 : " + LocalDateTime.now());
sync.syncMethod1("스레드 1");
System.out.println("스레드 1 종료 : " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드 2 시작 : " + LocalDateTime.now());
// sync.syncMethod2("스레드 2");
sync2.syncMethod2("스레드 2");
System.out.println("스레드 2 종료 : " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
<결과>
스레드 1 시작 : 2023-09-22T17:34:23.047693
스레드 2 시작 : 2023-09-22T17:34:23.047693
스레드 2의 syncMethod2 실행중2023-09-22T17:34:23.051694200
스레드 1의 syncMethod1 실행중2023-09-22T17:34:23.051694200
스레드 1 종료 : 2023-09-22T17:34:28.069353500
스레드 2 종료 : 2023-09-22T17:34:28.069353500
lock을 공유하지 않기 때문에 스레드간의 동기화가 발생하지 않는다.
하나의 인스턴스, lock
package org.example;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
public class SynchronizedMethod {
public static void main(String[] args) {
SynchronizedMethod sync = new SynchronizedMethod();
Thread thread1 = new Thread(() -> {
System.out.println("스레드 1 시작 : " + LocalDateTime.now());
sync.syncMethod1("스레드 1");
System.out.println("스레드 1 종료 : " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드 2 시작 : " + LocalDateTime.now());
sync.syncMethod2("스레드 2");
System.out.println("스레드 2 종료 : " + LocalDateTime.now());
});
Thread thread3 = new Thread(() -> {
System.out.println("스레드 3 시작 : " + LocalDateTime.now());
sync.method3("스레드 3");
System.out.println("스레드 3 종료 : " + LocalDateTime.now());
});
thread1.start();
thread2.start();
thread3.start();
}
private synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void method3(String msg) {
System.out.println(msg + "의 method3 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
<결과>
스레드 1 시작 : 2023-09-22T17:17:18.605913
스레드 2 시작 : 2023-09-22T17:17:18.605913
스레드 3 시작 : 2023-09-22T17:17:18.605913
스레드 1의 syncMethod1 실행중2023-09-22T17:17:18.608911100
스레드 3의 method3 실행중2023-09-22T17:17:18.608911100
스레드 3 종료 : 2023-09-22T17:17:23.615630300
스레드 2의 syncMethod2 실행중2023-09-22T17:17:23.615630300
스레드 1 종료 : 2023-09-22T17:17:23.615630300
스레드 2 종료 : 2023-09-22T17:17:28.622921200
synchronized 메소드는 인스턴스 단위로 lock을 걸고, synchronized 키워드가 붙은메소드들에 대해서만 lock을 공유한다.
- synchronized 메소드가 실행되면 다른 스레드들은 synchronized 메소드를 실행할 수 없다
- 일반 메소드는 실행할 수 있다.
static synchronized
public class StaticSynchronizedMethod {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
syncStaticMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
syncStaticMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
public static synchronized void syncStaticMethod1(String msg) {
System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void syncStaticMethod2(String msg) {
System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static 키워드가 포함된 synchronized 메소드는 인스턴스가 아닌 클래스 단위로 lock을 공유한다.
static이 없는 synchronized 메소드처럼 lock을 공유하여 메소드 간 동기화가 지켜진다.
<결과>
스레드1 시작 2023-09-22T17:20:27.319613600
스레드2 시작 2023-09-22T17:20:27.319613600
스레드1의 syncStaticMethod1 실행중2023-09-22T17:20:27.321614500
스레드1 종료 2023-09-22T17:20:32.327660400
스레드2의 syncStaticMethod2 실행중2023-09-22T17:20:32.327660400
스레드2 종료 2023-09-22T17:20:37.330105600
static synchronized + synchronized
public class StaticSynchronizedMethod {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
syncStaticMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
syncStaticMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
StaticSynchronizedMethod staticSync = new StaticSynchronizedMethod();
Thread thread3 = new Thread(() -> {
System.out.println("스레드3 시작 " + LocalDateTime.now());
staticSync.syncMethod3("스레드3");
System.out.println("스레드3 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
thread3.start();
}
public static synchronized void syncStaticMethod1(String msg) {
System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void syncStaticMethod2(String msg) {
System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncMethod3(String msg) {
System.out.println(msg + "의 syncMethod3 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
위에서 static이 없는 synchronized 메소드를 추가하였다.
<결과>
스레드1 시작 2023-09-22T17:20:27.319613600
스레드2 시작 2023-09-22T17:20:27.319613600
스레드3 시작 2023-09-22T17:20:27.319613600
스레드1의 syncStaticMethod1 실행중2023-09-22T17:20:27.321614500
스레드3의 syncMethod3 실행중2023-09-22T17:20:27.321614500
스레드3 종료 2023-09-22T17:20:32.327660400
스레드2의 syncStaticMethod2 실행중2023-09-22T17:20:32.327660400
스레드1 종료 2023-09-22T17:20:32.327660400
스레드2 종료 2023-09-22T17:20:37.330105600
static synchronized 메소드를 사용하는 스레드 1, 스레드 2는 동기화가 잘 지켜지지만 synchronized 메소드를 사용한 스레드 3은 동기화가 지켜지지 않았다.
이처럼 클래스 단위에 거든 lock과 인스턴스 단위에 거는 lock은 공유가 되지 않아 혼용해서 사용한다면 동기화 이슈가 발생한다.
Synchronized Block
public class SynchronizedBlock {
public static void main(String[] args) {
SynchronizedBlock block = new SynchronizedBlock();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
block.syncBlockMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
block.syncBlockMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private void syncBlockMethod1(String msg) {
synchronized (this) {
System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void syncBlockMethod2(String msg) {
synchronized (this) {
System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
<결과>
스레드1 시작 2023-09-22T17:22:20.322877200
스레드2 시작 2023-09-22T17:22:20.322877200
스레드1의 syncBlockMethod1 실행중2023-09-22T17:22:20.325900700
스레드1 종료 2023-09-22T17:22:25.333189200
스레드2의 syncBlockMethod2 실행중2023-09-22T17:22:25.333189200
스레드2 종료 2023-09-22T17:22:30.344752
synchronized 인자 값으로 this를 사용하면 모든 synchronized에 lock이 걸린다.
여러 스레드가 들어와서 서로 다른 synchronized block를 호출해도 this를 사용해 자기 자신에게 lock을 걸었으므로 동기화가 잘 지켜진다.
Synchronized Block(Object)
public class SynchronizedBlock2 {
private final Object o1 = new Object();
private final Object o2 = new Object();
public static void main(String[] args) {
SynchronizedBlock2 block = new SynchronizedBlock2();
Thread thread1 = new Thread(() -> {
System.out.println("스레드1 시작 " + LocalDateTime.now());
block.syncBlockMethod1("스레드1");
System.out.println("스레드1 종료 " + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("스레드2 시작 " + LocalDateTime.now());
block.syncBlockMethod2("스레드2");
System.out.println("스레드2 종료 " + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
private void syncBlockMethod1(String msg) {
synchronized (o1) {
System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void syncBlockMethod2(String msg) {
synchronized (o2) {
System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
<결과>
스레드1 시작 2023-09-22T17:22:49.629159100
스레드2 시작 2023-09-22T17:22:49.629159100
스레드2의 syncBlockMethod2 실행중2023-09-22T17:22:49.632676600
스레드1의 syncBlockMethod1 실행중2023-09-22T17:22:49.632676600
스레드1 종료 2023-09-22T17:22:54.647593
스레드2 종료 2023-09-22T17:22:54.647593
synchronized(this) 방식은 모든 블럭에 lock이 걸린다. 하지만 synchronized(Object) 방식으로 블록마다 다른 lock이 걸리게 할 수 있다.
이렇게 되면 각각의 스레드 안에서만 lock이 걸린다.
'Java' 카테고리의 다른 글
Java Executors, ExecutorService (0) | 2024.05.31 |
---|---|
OpenSearch를 사용하면서 발생한 문제 (0) | 2023.11.02 |
[JAVA] Socket 연결 시 Timeout 설정 (0) | 2023.10.20 |
[JAVA] GenericObjectPool에 대해 알아보고 사용해보자 (0) | 2023.10.17 |
[Java] try-with-resource (AutoCloseable) (0) | 2023.09.25 |